angular-my-report 0.1.0 → 0.1.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.
Files changed (88) hide show
  1. package/README.md +124 -40
  2. package/fesm2022/angular-my-report.mjs +7931 -0
  3. package/fesm2022/angular-my-report.mjs.map +1 -0
  4. package/index.d.ts +5 -0
  5. package/lib/engine/barcode.d.ts +17 -0
  6. package/lib/engine/chart.d.ts +26 -0
  7. package/lib/engine/chartLayout.d.ts +45 -0
  8. package/lib/engine/crosstab.d.ts +22 -0
  9. package/lib/engine/dataSources.d.ts +35 -0
  10. package/lib/engine/expressions.d.ts +46 -0
  11. package/lib/engine/measure.d.ts +7 -0
  12. package/lib/engine/parameters.d.ts +15 -0
  13. package/lib/engine/qr.d.ts +8 -0
  14. package/lib/engine/renderer.d.ts +43 -0
  15. package/lib/engine/table.d.ts +22 -0
  16. package/lib/model/defaults.d.ts +31 -0
  17. package/lib/model/factories.d.ts +31 -0
  18. package/lib/model/types.d.ts +244 -0
  19. package/lib/ui/css.d.ts +2 -0
  20. package/lib/ui/designer/banded-table-dialog.component.d.ts +27 -0
  21. package/lib/ui/designer/canvas.component.d.ts +41 -0
  22. package/lib/ui/designer/data-source-dialog.component.d.ts +37 -0
  23. package/lib/ui/designer/element-box.component.d.ts +32 -0
  24. package/lib/ui/designer/graphics.components.d.ts +36 -0
  25. package/lib/ui/designer/left-panel.component.d.ts +37 -0
  26. package/lib/ui/designer/safe-image.pipe.d.ts +13 -0
  27. package/lib/ui/designer/table-view.component.d.ts +19 -0
  28. package/lib/ui/designer.component.d.ts +59 -0
  29. package/lib/ui/designer.store.d.ts +82 -0
  30. package/lib/ui/inputs.d.ts +8 -0
  31. package/lib/ui/properties/band-props.component.d.ts +15 -0
  32. package/lib/ui/properties/element-props.component.d.ts +46 -0
  33. package/lib/ui/properties/expr-input.component.d.ts +43 -0
  34. package/lib/ui/properties/graphics-props.component.d.ts +52 -0
  35. package/lib/ui/properties/image-picker.component.d.ts +15 -0
  36. package/lib/ui/properties/properties-panel.component.d.ts +13 -0
  37. package/lib/ui/properties/report-extras.component.d.ts +40 -0
  38. package/lib/ui/properties/report-props.component.d.ts +34 -0
  39. package/lib/ui/properties/table-props.component.d.ts +31 -0
  40. package/lib/ui/viewer/page-view.component.d.ts +17 -0
  41. package/lib/ui/viewer/render.d.ts +43 -0
  42. package/lib/ui/viewer/viewer.component.d.ts +99 -0
  43. package/lib/ui/viewer.component.d.ts +40 -0
  44. package/package.json +16 -5
  45. package/{src/public-api.ts → public-api.d.ts} +0 -5
  46. package/ng-package.json +0 -7
  47. package/src/lib/engine/barcode.ts +0 -118
  48. package/src/lib/engine/chart.ts +0 -85
  49. package/src/lib/engine/chartLayout.ts +0 -265
  50. package/src/lib/engine/crosstab.ts +0 -96
  51. package/src/lib/engine/dataSources.ts +0 -204
  52. package/src/lib/engine/expressions.ts +0 -734
  53. package/src/lib/engine/measure.ts +0 -62
  54. package/src/lib/engine/parameters.ts +0 -96
  55. package/src/lib/engine/qr.ts +0 -26
  56. package/src/lib/engine/renderer.ts +0 -324
  57. package/src/lib/engine/table.ts +0 -58
  58. package/src/lib/model/defaults.ts +0 -249
  59. package/src/lib/model/factories.ts +0 -324
  60. package/src/lib/model/types.ts +0 -281
  61. package/src/lib/ui/css.ts +0 -12
  62. package/src/lib/ui/designer/banded-table-dialog.component.ts +0 -127
  63. package/src/lib/ui/designer/canvas.component.ts +0 -281
  64. package/src/lib/ui/designer/data-source-dialog.component.ts +0 -201
  65. package/src/lib/ui/designer/element-box.component.ts +0 -138
  66. package/src/lib/ui/designer/graphics.components.ts +0 -140
  67. package/src/lib/ui/designer/left-panel.component.ts +0 -189
  68. package/src/lib/ui/designer/safe-image.pipe.ts +0 -16
  69. package/src/lib/ui/designer/table-view.component.ts +0 -120
  70. package/src/lib/ui/designer.component.ts +0 -296
  71. package/src/lib/ui/designer.store.ts +0 -230
  72. package/src/lib/ui/inputs.ts +0 -34
  73. package/src/lib/ui/myreport.css +0 -267
  74. package/src/lib/ui/properties/band-props.component.ts +0 -86
  75. package/src/lib/ui/properties/element-props.component.ts +0 -326
  76. package/src/lib/ui/properties/expr-input.component.ts +0 -191
  77. package/src/lib/ui/properties/graphics-props.component.ts +0 -260
  78. package/src/lib/ui/properties/image-picker.component.ts +0 -62
  79. package/src/lib/ui/properties/properties-panel.component.ts +0 -37
  80. package/src/lib/ui/properties/report-extras.component.ts +0 -198
  81. package/src/lib/ui/properties/report-props.component.ts +0 -211
  82. package/src/lib/ui/properties/table-props.component.ts +0 -197
  83. package/src/lib/ui/viewer/page-view.component.ts +0 -73
  84. package/src/lib/ui/viewer/render.ts +0 -161
  85. package/src/lib/ui/viewer/viewer.component.ts +0 -555
  86. package/src/lib/ui/viewer.component.ts +0 -107
  87. package/tsconfig.lib.json +0 -12
  88. package/tsconfig.lib.prod.json +0 -5
@@ -1,211 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
2
- import { FormsModule } from '@angular/forms';
3
- import type { BandType, PageSettings, ReportDefinition } from '../../model/types';
4
- import { BAND_LABELS, PAGE_SIZES, createBand, uid } from '../../model/defaults';
5
- import { DesignerStore } from '../designer.store';
6
- import { ReportExtrasComponent } from './report-extras.component';
7
-
8
- export function removeGroup(r: ReportDefinition, gid: string): void {
9
- r.groups = r.groups.filter((g) => g.id !== gid);
10
- r.bands = r.bands.filter((b) => b.groupId !== gid);
11
- }
12
-
13
- const OPTIONAL_BANDS: BandType[] = ['reportHeader', 'pageHeader', 'detail', 'pageFooter', 'reportFooter'];
14
- type PageKey = Exclude<keyof PageSettings, 'continuous'>;
15
-
16
- @Component({
17
- selector: 'rpt-report-props',
18
- standalone: true,
19
- imports: [FormsModule, ReportExtrasComponent],
20
- changeDetection: ChangeDetectionStrategy.OnPush,
21
- template: `
22
- @let r = report();
23
- <h3>Report</h3>
24
- <section>
25
- <label class="prop-row"><span>Name</span>
26
- <div class="prop-value"><input [ngModel]="r.name" (ngModelChange)="setName($event)" /></div>
27
- </label>
28
- </section>
29
-
30
- <section>
31
- <h4>Page</h4>
32
- <label class="prop-row"><span>Size</span>
33
- <div class="prop-value">
34
- <select [ngModel]="sizeName()" (ngModelChange)="setSize($event)">
35
- @for (k of sizeNames; track k) {
36
- <option [value]="k">{{ k }}</option>
37
- }
38
- @if (sizeName() === 'Custom') {
39
- <option value="Custom">Custom</option>
40
- }
41
- </select>
42
- </div>
43
- </label>
44
- <label class="prop-row"><span>Orientation</span>
45
- <div class="prop-value">
46
- <select [ngModel]="landscape() ? 'landscape' : 'portrait'" (ngModelChange)="setOrientation($event)">
47
- <option value="portrait">Portrait</option>
48
- <option value="landscape">Landscape</option>
49
- </select>
50
- </div>
51
- </label>
52
- <div class="grid2">
53
- @for (f of pageFields; track f.key) {
54
- <label class="prop-row"><span>{{ f.label }}</span>
55
- <div class="prop-value">
56
- <input type="number" [min]="f.min" [ngModel]="r.page[f.key]" (ngModelChange)="setPage(f.key, f.min, $event)" />
57
- </div>
58
- </label>
59
- }
60
- </div>
61
- <label class="check" title="Receipt roll: one page as long as its content, no page breaks">
62
- <input type="checkbox" [ngModel]="!!r.page.continuous" (ngModelChange)="setContinuous($event)" /> Continuous (receipt roll, no page breaks)
63
- </label>
64
- <p class="hint">Units are pixels at 96 DPI (A4 = 794 × 1123, 80 mm receipt = 302 wide).</p>
65
- </section>
66
-
67
- <section>
68
- <h4>Sections</h4>
69
- @for (t of optionalBands; track t) {
70
- <label class="check">
71
- <input type="checkbox" [ngModel]="isVisible(t)" (ngModelChange)="setVisible(t, $event)" />
72
- {{ labels[t] }}
73
- </label>
74
- }
75
- <p class="hint">Without Detail, the report prints its header/footer once — fields there show the first/last record, or use sum(), count()… for totals.</p>
76
- </section>
77
-
78
- <section>
79
- <h4>Groups</h4>
80
- @for (g of r.groups; track g.id; let i = $index) {
81
- <div class="group-row">
82
- <span class="muted">{{ i + 1 }}.</span>
83
- <input list="field-names" placeholder="Field name" [ngModel]="g.field" (ngModelChange)="setGroupField(g.id, $event)" />
84
- <button class="small danger" title="Remove group" (click)="deleteGroup(g.id)">✕</button>
85
- </div>
86
- }
87
- <button (click)="addGroup()">+ Add group</button>
88
- </section>
89
-
90
- <section>
91
- <h4>Sort</h4>
92
- @for (s of r.sort; track $index; let i = $index) {
93
- <div class="group-row">
94
- <input list="field-names" placeholder="Field name" [ngModel]="s.field" (ngModelChange)="setSort(i, 'field', $event)" />
95
- <select [ngModel]="s.direction" (ngModelChange)="setSort(i, 'direction', $event)">
96
- <option value="asc">Asc</option>
97
- <option value="desc">Desc</option>
98
- </select>
99
- <button class="small danger" (click)="removeSort(i)">✕</button>
100
- </div>
101
- }
102
- <button (click)="addSort()">+ Add sort</button>
103
- </section>
104
-
105
- <rpt-report-extras />
106
- `,
107
- })
108
- export class ReportPropsComponent {
109
- private readonly store = inject(DesignerStore);
110
- protected readonly report = this.store.report;
111
- protected readonly labels = BAND_LABELS;
112
- protected readonly optionalBands = OPTIONAL_BANDS;
113
- protected readonly sizeNames = Object.keys(PAGE_SIZES);
114
- protected readonly pageFields: { key: PageKey; label: string; min: number }[] = [
115
- { key: 'width', label: 'Width', min: 100 },
116
- { key: 'height', label: 'Height', min: 100 },
117
- { key: 'marginTop', label: 'Margin Top', min: 0 },
118
- { key: 'marginBottom', label: 'Margin Bottom', min: 0 },
119
- { key: 'marginLeft', label: 'Margin Left', min: 0 },
120
- { key: 'marginRight', label: 'Margin Right', min: 0 },
121
- ];
122
-
123
- protected readonly landscape = computed(() => this.report().page.width > this.report().page.height);
124
- protected readonly sizeName = computed(() => {
125
- const p = this.report().page;
126
- const hit = Object.entries(PAGE_SIZES).find(
127
- ([, s]) => (s.width === p.width && s.height === p.height) || (s.width === p.height && s.height === p.width),
128
- );
129
- return hit?.[0] ?? 'Custom';
130
- });
131
-
132
- protected setName(v: string): void {
133
- this.store.update((r) => (r.name = v));
134
- }
135
-
136
- protected setSize(name: string): void {
137
- const s = PAGE_SIZES[name];
138
- if (!s) return;
139
- const land = this.landscape();
140
- const receipt = name.startsWith('Receipt');
141
- this.store.update((r) => {
142
- r.page.width = land && !receipt ? s.height : s.width;
143
- r.page.height = land && !receipt ? s.width : s.height;
144
- if (receipt) {
145
- // Thermal roll: narrow margins, page as long as the content.
146
- r.page.continuous = true;
147
- r.page.marginLeft = r.page.marginRight = name.includes('58') ? 6 : 10;
148
- r.page.marginTop = 10;
149
- r.page.marginBottom = 14;
150
- } else r.page.continuous = false;
151
- });
152
- }
153
-
154
- protected setContinuous(on: boolean): void {
155
- this.store.update((r) => (r.page.continuous = on));
156
- }
157
-
158
- protected setOrientation(v: string): void {
159
- if ((v === 'landscape') !== this.landscape()) this.store.update((r) => ([r.page.width, r.page.height] = [r.page.height, r.page.width]));
160
- }
161
-
162
- protected setPage(k: PageKey, min: number, v: number | null): void {
163
- if (v != null && Number.isFinite(v)) this.store.update((r) => (r.page[k] = Math.max(min, v)));
164
- }
165
-
166
- protected isVisible(t: BandType): boolean {
167
- return !!this.report().bands.find((b) => b.type === t)?.visible;
168
- }
169
-
170
- protected setVisible(t: BandType, on: boolean): void {
171
- this.store.update((r) => {
172
- const ex = r.bands.find((x) => x.type === t);
173
- if (ex) ex.visible = on;
174
- else r.bands.push(createBand(t, 30));
175
- });
176
- }
177
-
178
- protected setGroupField(gid: string, v: string): void {
179
- this.store.update((r) => {
180
- const g = r.groups.find((x) => x.id === gid);
181
- if (g) g.field = v;
182
- });
183
- }
184
-
185
- protected deleteGroup(gid: string): void {
186
- this.store.update((r) => removeGroup(r, gid));
187
- }
188
-
189
- protected addGroup(): void {
190
- this.store.update((r) => {
191
- const id = uid('grp');
192
- r.groups.push({ id, field: '', direction: 'asc' });
193
- r.bands.push(createBand('groupHeader', 26, id), createBand('groupFooter', 26, id));
194
- });
195
- }
196
-
197
- protected setSort(i: number, k: 'field' | 'direction', v: string): void {
198
- this.store.update((r) => {
199
- if (k === 'field') r.sort[i].field = v;
200
- else r.sort[i].direction = v as 'asc' | 'desc';
201
- });
202
- }
203
-
204
- protected removeSort(i: number): void {
205
- this.store.update((r) => r.sort.splice(i, 1));
206
- }
207
-
208
- protected addSort(): void {
209
- this.store.update((r) => r.sort.push({ field: '', direction: 'asc' }));
210
- }
211
- }
@@ -1,197 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
2
- import { FormsModule } from '@angular/forms';
3
- import { ExprInputComponent } from './expr-input.component';
4
- import type { FormatKind, HAlign, ReportElement, TableColumn, TableSpec } from '../../model/types';
5
- import { columnWidths, createColumn, designTableHeight } from '../../engine/table';
6
- import { DesignerStore, findElement } from '../designer.store';
7
-
8
- const FORMAT_KINDS: FormatKind[] = ['none', 'number', 'currency', 'percent', 'date', 'datetime', 'time'];
9
- type ColorKey = 'headerBackground' | 'stripeBackground' | 'gridColor' | 'headerColor';
10
-
11
- @Component({
12
- selector: 'rpt-table-props',
13
- standalone: true,
14
- imports: [FormsModule, ExprInputComponent],
15
- changeDetection: ChangeDetectionStrategy.OnPush,
16
- template: `
17
- @let t = spec();
18
- <section>
19
- <h4>Table</h4>
20
- <label class="check">
21
- <input type="checkbox" [ngModel]="t.showHeader" (ngModelChange)="set(flag('showHeader', $event))" /> Header row
22
- </label>
23
- <label class="check">
24
- <input type="checkbox" [ngModel]="t.repeatHeader" (ngModelChange)="set(flag('repeatHeader', $event))" /> Repeat header on each page
25
- </label>
26
- <label class="check">
27
- <input type="checkbox" [ngModel]="t.showFooter" (ngModelChange)="set(flag('showFooter', $event))" /> Footer (totals) row
28
- </label>
29
- <div class="grid2">
30
- <label class="prop-row"><span>Header h</span>
31
- <div class="prop-value"><input type="number" min="8" [ngModel]="t.headerHeight" (ngModelChange)="height('headerHeight', $event)" /></div>
32
- </label>
33
- <label class="prop-row"><span>Row h</span>
34
- <div class="prop-value"><input type="number" min="8" [ngModel]="t.rowHeight" (ngModelChange)="height('rowHeight', $event)" /></div>
35
- </label>
36
- </div>
37
- <div class="prop-row"><span>Header fill</span>
38
- <div class="prop-value">
39
- <input type="color" [ngModel]="colorValue(t.headerBackground)" (ngModelChange)="set(color('headerBackground', $event))" />
40
- <button class="small" [class.on]="t.headerBackground === 'transparent'" (click)="set(color('headerBackground', 'transparent'))">None</button>
41
- </div>
42
- </div>
43
- <div class="prop-row"><span>Header text</span>
44
- <div class="prop-value"><input type="color" [ngModel]="t.headerColor" (ngModelChange)="set(color('headerColor', $event))" /></div>
45
- </div>
46
- <div class="prop-row"><span>Row fill</span>
47
- <div class="prop-value">
48
- <input type="color" [ngModel]="colorValue(el().style.background)" (ngModelChange)="set(rowFill($event))" />
49
- <button class="small" [class.on]="el().style.background === 'transparent'" (click)="set(rowFill('transparent'))">None</button>
50
- </div>
51
- </div>
52
- <div class="prop-row"><span>Alt. row</span>
53
- <div class="prop-value">
54
- <input type="color" [ngModel]="colorValue(t.stripeBackground)" (ngModelChange)="set(color('stripeBackground', $event))" />
55
- <button class="small" [class.on]="t.stripeBackground === 'transparent'" (click)="set(color('stripeBackground', 'transparent'))">None</button>
56
- </div>
57
- </div>
58
- <div class="prop-row"><span>Grid lines</span>
59
- <div class="prop-value">
60
- <input type="color" [ngModel]="colorValue(t.gridColor)" (ngModelChange)="set(color('gridColor', $event))" />
61
- <button class="small" [class.on]="t.gridColor === 'transparent'" (click)="set(color('gridColor', 'transparent'))">None</button>
62
- </div>
63
- </div>
64
- <p class="hint">
65
- Lists the rows of its band: all rows in Report Header/Footer, the group's rows in Group bands. It continues onto new pages
66
- automatically. Drop a field on the table to add a column.
67
- </p>
68
- </section>
69
-
70
- <section>
71
- <h4>Columns</h4>
72
- @for (c of t.columns; track c.id; let i = $index, last = $last) {
73
- <div class="col-card">
74
- <div class="col-head">
75
- <strong>{{ i + 1 }}</strong>
76
- <span class="muted">{{ widths()[i] }}px</span>
77
- <span class="spacer"></span>
78
- <button class="small" [disabled]="i === 0" title="Move left" (click)="moveColumn(i, -1)">←</button>
79
- <button class="small" [disabled]="last" title="Move right" (click)="moveColumn(i, 1)">→</button>
80
- <button class="small danger" title="Remove column" (click)="removeColumn(i)">✕</button>
81
- </div>
82
- <label class="prop-row"><span>Header</span>
83
- <div class="prop-value"><input [ngModel]="c.header" (ngModelChange)="col(i, setHeader($event))" /></div>
84
- </label>
85
- <label class="prop-row"><span>Cell</span>
86
- <div class="prop-value"><rpt-expr-input [value]="c.text" placeholder="{Field}" (valueChange)="col(i, setCell($event))" /></div>
87
- </label>
88
- @if (t.showFooter) {
89
- <label class="prop-row"><span>Footer</span>
90
- <div class="prop-value"><rpt-expr-input [value]="c.footer" placeholder="{sum(Amount)}" (valueChange)="col(i, setFooter($event))" /></div>
91
- </label>
92
- }
93
- <div class="grid2">
94
- <label class="prop-row"><span>Width</span>
95
- <div class="prop-value">
96
- <input type="number" min="0.1" step="0.1" [ngModel]="c.width" (ngModelChange)="colWidth(i, $event)" />
97
- </div>
98
- </label>
99
- <label class="prop-row"><span>Align</span>
100
- <div class="prop-value">
101
- <select [ngModel]="c.align" (ngModelChange)="col(i, setAlign($event))">
102
- <option value="left">Left</option>
103
- <option value="center">Center</option>
104
- <option value="right">Right</option>
105
- </select>
106
- </div>
107
- </label>
108
- </div>
109
- <div class="grid2">
110
- <label class="prop-row"><span>Format</span>
111
- <div class="prop-value">
112
- <select [ngModel]="c.format.kind" (ngModelChange)="col(i, setFormat($event))">
113
- @for (k of formatKinds; track k) {
114
- <option [value]="k">{{ k }}</option>
115
- }
116
- </select>
117
- </div>
118
- </label>
119
- @if (c.format.kind === 'number' || c.format.kind === 'currency' || c.format.kind === 'percent') {
120
- <label class="prop-row"><span>Decimals</span>
121
- <div class="prop-value">
122
- <input type="number" min="0" [ngModel]="c.format.decimals" (ngModelChange)="colDecimals(i, $event)" />
123
- </div>
124
- </label>
125
- }
126
- </div>
127
- </div>
128
- }
129
- <button (click)="addColumn()">+ Add column</button>
130
- </section>
131
- `,
132
- })
133
- export class TablePropsComponent {
134
- private readonly store = inject(DesignerStore);
135
- readonly el = input.required<ReportElement>();
136
-
137
- protected readonly formatKinds = FORMAT_KINDS;
138
- protected readonly spec = computed(() => this.el().table!);
139
- protected readonly widths = computed(() => columnWidths(this.el()).map((w) => Math.round(w)));
140
-
141
- /** Edits the table and keeps the element height in sync with its header/row/footer heights. */
142
- protected set(fn: (t: TableSpec, e: ReportElement) => void): void {
143
- const id = this.el().id;
144
- this.store.update((r) => {
145
- const f = findElement(r, id);
146
- if (!f?.el.table) return;
147
- fn(f.el.table, f.el);
148
- f.el.h = designTableHeight(f.el.table);
149
- f.band.height = Math.max(f.band.height, f.el.y + f.el.h);
150
- });
151
- }
152
-
153
- protected col(i: number, fn: (c: TableColumn) => void): void {
154
- this.set((t) => t.columns[i] && fn(t.columns[i]));
155
- }
156
-
157
- protected colorValue(v: string): string {
158
- return !v || v === 'transparent' ? '#ffffff' : v;
159
- }
160
-
161
- protected flag = (k: 'showHeader' | 'repeatHeader' | 'showFooter', v: boolean) => (t: TableSpec) => (t[k] = v);
162
- protected color = (k: ColorKey, v: string) => (t: TableSpec) => (t[k] = v);
163
- protected rowFill = (v: string) => (_: TableSpec, e: ReportElement) => (e.style.background = v);
164
- protected setHeader = (v: string) => (c: TableColumn) => (c.header = v);
165
- protected setCell = (v: string) => (c: TableColumn) => (c.text = v);
166
- protected setFooter = (v: string) => (c: TableColumn) => (c.footer = v);
167
- protected setAlign = (v: HAlign) => (c: TableColumn) => (c.align = v);
168
- protected setFormat = (v: FormatKind) => (c: TableColumn) => (c.format.kind = v);
169
-
170
- protected height(k: 'headerHeight' | 'rowHeight', v: number | null): void {
171
- if (v != null && Number.isFinite(v)) this.set((t) => (t[k] = Math.max(8, v)));
172
- }
173
-
174
- protected colWidth(i: number, v: number | null): void {
175
- if (v != null && Number.isFinite(v)) this.col(i, (c) => (c.width = Math.max(0.1, v)));
176
- }
177
-
178
- protected colDecimals(i: number, v: number | null): void {
179
- if (v != null && Number.isFinite(v)) this.col(i, (c) => (c.format.decimals = Math.max(0, Math.min(10, v))));
180
- }
181
-
182
- protected moveColumn(i: number, d: number): void {
183
- this.set((t) => {
184
- const j = i + d;
185
- if (j < 0 || j >= t.columns.length) return;
186
- [t.columns[i], t.columns[j]] = [t.columns[j], t.columns[i]];
187
- });
188
- }
189
-
190
- protected removeColumn(i: number): void {
191
- this.set((t) => t.columns.splice(i, 1));
192
- }
193
-
194
- protected addColumn(): void {
195
- this.set((t) => t.columns.push(createColumn('')));
196
- }
197
- }
@@ -1,73 +0,0 @@
1
- import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
- import { NgTemplateOutlet } from '@angular/common';
3
- import type { PageBackground, PageSettings, Watermark } from '../../model/types';
4
- import { contentWidth } from '../../model/defaults';
5
- import { ElementBoxComponent } from '../designer/element-box.component';
6
- import { SafeImagePipe } from '../designer/safe-image.pipe';
7
- import type { ViewPage } from './render';
8
-
9
- const FIT = { cover: 'cover', contain: 'contain', stretch: 'fill', center: 'none' } as const;
10
-
11
- /** One printed page: background, watermark, bands and their elements. */
12
- @Component({
13
- selector: 'rpt-page-view',
14
- standalone: true,
15
- imports: [NgTemplateOutlet, ElementBoxComponent, SafeImagePipe],
16
- changeDetection: ChangeDetectionStrategy.OnPush,
17
- template: `
18
- @let p = settings();
19
- <div class="rpt-page" [class.roll]="p.continuous" [style.width.px]="p.width" [style.height.px]="page().height">
20
- @if (page().background) {
21
- <img class="rpt-bg" alt="" [src]="background().image | safeImage" [style.object-fit]="fit()" />
22
- }
23
- @if (page().watermark && watermark().layer === 'behind') {
24
- <ng-container *ngTemplateOutlet="wm" />
25
- }
26
- @for (b of page().bands; track $index) {
27
- <div style="position: absolute" [style.left.px]="p.marginLeft" [style.top.px]="b.top" [style.width.px]="contentWidth()" [style.height.px]="b.height">
28
- @for (it of b.items; track it.key) {
29
- <rpt-element
30
- [el]="it.el"
31
- [top]="it.top"
32
- [height]="it.height"
33
- [content]="it.content"
34
- [tableData]="it.tableData"
35
- [rows]="it.rows"
36
- [attr.data-key]="it.key"
37
- [class.find-hit]="highlights().has(it.key)"
38
- [class.find-active]="active() === it.key"
39
- />
40
- }
41
- </div>
42
- }
43
- @if (page().watermark && watermark().layer === 'front') {
44
- <ng-container *ngTemplateOutlet="wm" />
45
- }
46
- </div>
47
-
48
- <ng-template #wm>
49
- @let w = watermark();
50
- <div class="rpt-watermark" [style.opacity]="w.opacity">
51
- @if (w.kind === 'image') {
52
- <img alt="" [src]="w.image | safeImage" [style.width.%]="w.imageScale" [style.transform]="'rotate(' + w.angle + 'deg)'" />
53
- } @else {
54
- <span [style.font-size.px]="w.fontSize" [style.color]="w.color" [style.font-weight]="w.bold ? 700 : 400" [style.transform]="'rotate(' + w.angle + 'deg)'">{{
55
- watermarkText()
56
- }}</span>
57
- }
58
- </div>
59
- </ng-template>
60
- `,
61
- })
62
- export class PageViewComponent {
63
- readonly page = input.required<ViewPage>();
64
- readonly settings = input.required<PageSettings>();
65
- readonly watermark = input.required<Watermark>();
66
- readonly background = input.required<PageBackground>();
67
- readonly watermarkText = input('');
68
- readonly highlights = input<ReadonlySet<string>>(new Set());
69
- readonly active = input<string | null>(null);
70
-
71
- protected readonly contentWidth = computed(() => contentWidth(this.settings()));
72
- protected readonly fit = computed(() => FIT[this.background().fit]);
73
- }
@@ -1,161 +0,0 @@
1
- import type { ElementStyle, ReportDefinition, ReportElement } from '../../model/types';
2
- import { defaultBackground, defaultWatermark } from '../../model/defaults';
3
- import { contextFor, paginate, type PlacedElement } from '../../engine/renderer';
4
- import { evaluate, getPath, renderTemplate, truthy, type EvalContext, type Row } from '../../engine/expressions';
5
- import type { TableRenderData } from '../../engine/table';
6
- import type { ParamValues } from '../../engine/parameters';
7
-
8
- export interface ViewItem {
9
- /** Unique within the document: page:band:element. Used for find highlights. */
10
- key: string;
11
- el: ReportElement;
12
- rows?: Row[];
13
- top: number;
14
- height: number;
15
- content: string;
16
- tableData?: TableRenderData;
17
- /** Plain text of the item (content or table cells), for Find. */
18
- text: string;
19
- }
20
-
21
- export interface ViewBand {
22
- top: number;
23
- height: number;
24
- items: ViewItem[];
25
- }
26
-
27
- export interface ViewPage {
28
- index: number;
29
- height: number;
30
- background: boolean;
31
- watermark: boolean;
32
- bands: ViewBand[];
33
- }
34
-
35
- /** Group headers as a navigable tree. */
36
- export interface Bookmark {
37
- label: string;
38
- page: number;
39
- top: number;
40
- children: Bookmark[];
41
- }
42
-
43
- export interface ViewDocument {
44
- pages: ViewPage[];
45
- bookmarks: Bookmark[];
46
- rowCount: number;
47
- watermarkText: string;
48
- }
49
-
50
- const NO_FORMAT = { kind: 'none', decimals: 0, currency: '' } as const;
51
-
52
- /** Condition check that treats an invalid expression as "true" (never hides content by accident). */
53
- function holds(expr: string | undefined, ctx: EvalContext): boolean {
54
- if (!expr?.trim()) return true;
55
- try {
56
- return truthy(evaluate(expr, ctx));
57
- } catch {
58
- return true;
59
- }
60
- }
61
-
62
- /** Evaluates the visible slice of a table into cell strings. */
63
- function tableData(item: PlacedElement, ctx: EvalContext): TableRenderData | undefined {
64
- const p = item.table;
65
- const t = item.el.table;
66
- if (!p || !t) return undefined;
67
- const all = ctx.rows;
68
- return {
69
- header: p.header ? t.columns.map((c) => renderTemplate(c.header, ctx, NO_FORMAT)) : null,
70
- headerHeight: p.headerHeight,
71
- rows: p.rows.map((row, i) => {
72
- const rctx = { ...ctx, row, rowNumber: p.startIndex + i + 1 };
73
- return { cells: t.columns.map((c) => renderTemplate(c.text, rctx, c.format)), height: p.rowHeights[i], index: p.startIndex + i };
74
- }),
75
- footer: p.footer ? t.columns.map((c) => renderTemplate(c.footer, { ...ctx, rows: all, row: all[all.length - 1] }, c.format)) : null,
76
- footerHeight: p.footerHeight,
77
- };
78
- }
79
-
80
- function evaluateItem(key: string, it: PlacedElement, ctx: EvalContext, lastValue: Map<string, string>): ViewItem {
81
- let el: ReportElement = it.el;
82
- let content = ['line', 'rect', 'table', 'chart'].includes(el.kind) ? '' : renderTemplate(el.text, ctx, el.format);
83
-
84
- const opts = el.fieldOptions;
85
- if (opts && (el.kind === 'text' || el.kind === 'field')) {
86
- if (!content.trim() && opts.nullText) content = renderTemplate(opts.nullText, ctx, el.format);
87
- if (opts.hideDuplicates) {
88
- if (lastValue.get(el.id) === content) content = '';
89
- else lastValue.set(el.id, content);
90
- }
91
- const rule = opts.rules.find((r) => r.when.trim() && holds(r.when, ctx));
92
- if (rule) {
93
- const style: Partial<ElementStyle> = { color: rule.color };
94
- if (rule.background) style.background = rule.background;
95
- if (rule.bold) style.bold = true;
96
- el = { ...el, style: { ...el.style, ...style } };
97
- }
98
- }
99
- // Chart titles may contain fields, e.g. "Sales in {Country}".
100
- if (el.chart) el = { ...el, chart: { ...el.chart, title: renderTemplate(el.chart.title, ctx, NO_FORMAT) } };
101
-
102
- const td = tableData(it, ctx);
103
- const text =
104
- el.kind === 'barcode' || el.kind === 'qrcode' || el.kind === 'image'
105
- ? ''
106
- : td
107
- ? [...(td.header ?? []), ...td.rows.flatMap((r) => r.cells), ...(td.footer ?? [])].join(' ')
108
- : el.chart?.title ?? content;
109
- return { key, el, rows: el.kind === 'chart' ? ctx.rows : undefined, top: it.top, height: it.height, content, tableData: td, text };
110
- }
111
-
112
- /** Lays out and evaluates the whole report into ready-to-draw pages, plus group bookmarks. */
113
- export function buildDocument(report: ReportDefinition, rows: Row[], params: ParamValues, root: unknown): ViewDocument {
114
- const layout = paginate(report, rows, params, root);
115
- const wm = { ...defaultWatermark(), ...report.watermark };
116
- const bg = { ...defaultBackground(), ...report.background };
117
-
118
- const pages: ViewPage[] = layout.map((pg) => {
119
- const lastValue = new Map<string, string>(); // hide-duplicates state, per page
120
- return {
121
- index: pg.index,
122
- height: pg.height ?? report.page.height,
123
- background: bg.enabled && !!bg.image && (bg.pages === 'all' || pg.index === 0),
124
- watermark: wm.enabled && (wm.kind === 'image' ? !!wm.image : !!wm.text) && (wm.pages === 'all' || pg.index === 0),
125
- bands: pg.bands.map((pb, bi) => {
126
- const ctx = contextFor(pb, pg, layout.length);
127
- return {
128
- top: pb.top,
129
- height: pb.height,
130
- items: pb.items
131
- .filter((it) => holds(it.el.visibleWhen, ctx))
132
- .map((it) => evaluateItem(`${pg.index}:${bi}:${it.el.id}`, it, ctx, lastValue)),
133
- };
134
- }),
135
- };
136
- });
137
-
138
- // Bookmarks: one per group header, nested by group level.
139
- const bookmarks: Bookmark[] = [];
140
- const stack: Bookmark[] = [];
141
- for (const pg of layout) {
142
- for (const pb of pg.bands) {
143
- if (pb.band.type !== 'groupHeader' || !pb.band.groupId) continue;
144
- const level = report.groups.findIndex((g) => g.id === pb.band.groupId);
145
- if (level < 0) continue;
146
- const g = report.groups[level];
147
- const bm: Bookmark = { label: String(getPath(pb.ctx.row, g.field) ?? '(blank)'), page: pg.index, top: pb.top, children: [] };
148
- stack.length = level;
149
- (level > 0 && stack[level - 1] ? stack[level - 1].children : bookmarks).push(bm);
150
- stack[level] = bm;
151
- }
152
- }
153
-
154
- const ctx: EvalContext = { row: undefined, rows: [], allRows: [], pageRows: [], rowNumber: 0, pageNumber: 1, totalPages: 1, reportName: report.name, params, root };
155
- return {
156
- pages,
157
- bookmarks,
158
- rowCount: layout.reduce((n, p) => n + p.rows.length, 0),
159
- watermarkText: renderTemplate(wm.text, ctx, NO_FORMAT),
160
- };
161
- }