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.
- package/README.md +124 -40
- package/fesm2022/angular-my-report.mjs +7931 -0
- package/fesm2022/angular-my-report.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/engine/barcode.d.ts +17 -0
- package/lib/engine/chart.d.ts +26 -0
- package/lib/engine/chartLayout.d.ts +45 -0
- package/lib/engine/crosstab.d.ts +22 -0
- package/lib/engine/dataSources.d.ts +35 -0
- package/lib/engine/expressions.d.ts +46 -0
- package/lib/engine/measure.d.ts +7 -0
- package/lib/engine/parameters.d.ts +15 -0
- package/lib/engine/qr.d.ts +8 -0
- package/lib/engine/renderer.d.ts +43 -0
- package/lib/engine/table.d.ts +22 -0
- package/lib/model/defaults.d.ts +31 -0
- package/lib/model/factories.d.ts +31 -0
- package/lib/model/types.d.ts +244 -0
- package/lib/ui/css.d.ts +2 -0
- package/lib/ui/designer/banded-table-dialog.component.d.ts +27 -0
- package/lib/ui/designer/canvas.component.d.ts +41 -0
- package/lib/ui/designer/data-source-dialog.component.d.ts +37 -0
- package/lib/ui/designer/element-box.component.d.ts +32 -0
- package/lib/ui/designer/graphics.components.d.ts +36 -0
- package/lib/ui/designer/left-panel.component.d.ts +37 -0
- package/lib/ui/designer/safe-image.pipe.d.ts +13 -0
- package/lib/ui/designer/table-view.component.d.ts +19 -0
- package/lib/ui/designer.component.d.ts +59 -0
- package/lib/ui/designer.store.d.ts +82 -0
- package/lib/ui/inputs.d.ts +8 -0
- package/lib/ui/properties/band-props.component.d.ts +15 -0
- package/lib/ui/properties/element-props.component.d.ts +46 -0
- package/lib/ui/properties/expr-input.component.d.ts +43 -0
- package/lib/ui/properties/graphics-props.component.d.ts +52 -0
- package/lib/ui/properties/image-picker.component.d.ts +15 -0
- package/lib/ui/properties/properties-panel.component.d.ts +13 -0
- package/lib/ui/properties/report-extras.component.d.ts +40 -0
- package/lib/ui/properties/report-props.component.d.ts +34 -0
- package/lib/ui/properties/table-props.component.d.ts +31 -0
- package/lib/ui/viewer/page-view.component.d.ts +17 -0
- package/lib/ui/viewer/render.d.ts +43 -0
- package/lib/ui/viewer/viewer.component.d.ts +99 -0
- package/lib/ui/viewer.component.d.ts +40 -0
- package/package.json +16 -5
- package/{src/public-api.ts → public-api.d.ts} +0 -5
- package/ng-package.json +0 -7
- package/src/lib/engine/barcode.ts +0 -118
- package/src/lib/engine/chart.ts +0 -85
- package/src/lib/engine/chartLayout.ts +0 -265
- package/src/lib/engine/crosstab.ts +0 -96
- package/src/lib/engine/dataSources.ts +0 -204
- package/src/lib/engine/expressions.ts +0 -734
- package/src/lib/engine/measure.ts +0 -62
- package/src/lib/engine/parameters.ts +0 -96
- package/src/lib/engine/qr.ts +0 -26
- package/src/lib/engine/renderer.ts +0 -324
- package/src/lib/engine/table.ts +0 -58
- package/src/lib/model/defaults.ts +0 -249
- package/src/lib/model/factories.ts +0 -324
- package/src/lib/model/types.ts +0 -281
- package/src/lib/ui/css.ts +0 -12
- package/src/lib/ui/designer/banded-table-dialog.component.ts +0 -127
- package/src/lib/ui/designer/canvas.component.ts +0 -281
- package/src/lib/ui/designer/data-source-dialog.component.ts +0 -201
- package/src/lib/ui/designer/element-box.component.ts +0 -138
- package/src/lib/ui/designer/graphics.components.ts +0 -140
- package/src/lib/ui/designer/left-panel.component.ts +0 -189
- package/src/lib/ui/designer/safe-image.pipe.ts +0 -16
- package/src/lib/ui/designer/table-view.component.ts +0 -120
- package/src/lib/ui/designer.component.ts +0 -296
- package/src/lib/ui/designer.store.ts +0 -230
- package/src/lib/ui/inputs.ts +0 -34
- package/src/lib/ui/myreport.css +0 -267
- package/src/lib/ui/properties/band-props.component.ts +0 -86
- package/src/lib/ui/properties/element-props.component.ts +0 -326
- package/src/lib/ui/properties/expr-input.component.ts +0 -191
- package/src/lib/ui/properties/graphics-props.component.ts +0 -260
- package/src/lib/ui/properties/image-picker.component.ts +0 -62
- package/src/lib/ui/properties/properties-panel.component.ts +0 -37
- package/src/lib/ui/properties/report-extras.component.ts +0 -198
- package/src/lib/ui/properties/report-props.component.ts +0 -211
- package/src/lib/ui/properties/table-props.component.ts +0 -197
- package/src/lib/ui/viewer/page-view.component.ts +0 -73
- package/src/lib/ui/viewer/render.ts +0 -161
- package/src/lib/ui/viewer/viewer.component.ts +0 -555
- package/src/lib/ui/viewer.component.ts +0 -107
- package/tsconfig.lib.json +0 -12
- package/tsconfig.lib.prod.json +0 -5
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
|
|
2
|
-
import { FormsModule } from '@angular/forms';
|
|
3
|
-
import type { ElementStyle, FormatKind, ReportElement, StyleRule } from '../../model/types';
|
|
4
|
-
import { contentWidth, defaultFieldOptions } from '../../model/defaults';
|
|
5
|
-
import { AGGREGATES } from '../../engine/expressions';
|
|
6
|
-
import { DesignerStore } from '../designer.store';
|
|
7
|
-
import { TablePropsComponent } from './table-props.component';
|
|
8
|
-
import { ExprInputComponent } from './expr-input.component';
|
|
9
|
-
import { ImagePickerComponent } from './image-picker.component';
|
|
10
|
-
import { BarcodePropsComponent, ChartPropsComponent, CrosstabPropsComponent, QrPropsComponent } from './graphics-props.component';
|
|
11
|
-
|
|
12
|
-
export const FORMAT_KINDS: FormatKind[] = ['none', 'number', 'currency', 'percent', 'date', 'datetime', 'time'];
|
|
13
|
-
const NUMERIC_FORMATS = ['number', 'currency', 'percent'];
|
|
14
|
-
|
|
15
|
-
@Component({
|
|
16
|
-
selector: 'rpt-element-props',
|
|
17
|
-
standalone: true,
|
|
18
|
-
imports: [FormsModule, ExprInputComponent, ImagePickerComponent, TablePropsComponent, ChartPropsComponent, BarcodePropsComponent, QrPropsComponent, CrosstabPropsComponent],
|
|
19
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
|
-
template: `
|
|
21
|
-
@let e = el();
|
|
22
|
-
@let s = e.style;
|
|
23
|
-
<h3>
|
|
24
|
-
{{ title() }}
|
|
25
|
-
@if (count() > 1) {
|
|
26
|
-
<span class="muted">({{ count() }} selected)</span>
|
|
27
|
-
}
|
|
28
|
-
</h3>
|
|
29
|
-
|
|
30
|
-
@if (e.kind === 'image') {
|
|
31
|
-
<section>
|
|
32
|
-
<h4>Image</h4>
|
|
33
|
-
<rpt-image-picker [value]="e.text" (valueChange)="set(setText($event))" />
|
|
34
|
-
<label class="prop-row"><span>Fit</span>
|
|
35
|
-
<div class="prop-value">
|
|
36
|
-
<select [ngModel]="e.imageFit ?? 'contain'" (ngModelChange)="set(setFit($event))">
|
|
37
|
-
<option value="contain">Contain (whole image)</option>
|
|
38
|
-
<option value="cover">Cover (fill, crop)</option>
|
|
39
|
-
<option value="fill">Stretch</option>
|
|
40
|
-
</select>
|
|
41
|
-
</div>
|
|
42
|
-
</label>
|
|
43
|
-
</section>
|
|
44
|
-
}
|
|
45
|
-
@if (isText() || isCode()) {
|
|
46
|
-
<section>
|
|
47
|
-
<div class="prop-label">{{ isCode() ? 'Value' : 'Text' }} <span class="muted">— type {{ '{' }} for fields & formulas, Ctrl+Space for suggestions</span></div>
|
|
48
|
-
<rpt-expr-input [value]="e.text" [rows]="3" (valueChange)="set(setText($event))" />
|
|
49
|
-
@if (isCode()) {
|
|
50
|
-
<p class="hint">Use fields, e.g. <code>{{ '{' }}OrderID{{ '}' }}</code>. The designer shows the first data row.</p>
|
|
51
|
-
}
|
|
52
|
-
@if (isText()) {
|
|
53
|
-
<p class="hint">
|
|
54
|
-
e.g. <code>{{ '{' }}Quantity * UnitPrice{{ '}' }}</code>, <code>{{ '{' }}sum(Amount, report){{ '}' }}</code>,
|
|
55
|
-
<code>{{ '{' }}iif(Amount > 100, "High", "Low"){{ '}' }}</code>, <code>{{ '{' }}upper(Customer){{ '}' }}</code>,
|
|
56
|
-
<code>{{ '{' }}@Country{{ '}' }}</code> (parameter). Outside Detail, a field shows the first record of its band.
|
|
57
|
-
</p>
|
|
58
|
-
}
|
|
59
|
-
</section>
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
<section>
|
|
63
|
-
<h4>Layout</h4>
|
|
64
|
-
<div class="grid2">
|
|
65
|
-
<label class="prop-row"><span>X</span>
|
|
66
|
-
<div class="prop-value"><input type="number" min="0" [ngModel]="e.x" (ngModelChange)="num($event, setX)" /></div>
|
|
67
|
-
</label>
|
|
68
|
-
<label class="prop-row"><span>Y</span>
|
|
69
|
-
<div class="prop-value"><input type="number" min="0" [ngModel]="e.y" (ngModelChange)="num($event, setY)" /></div>
|
|
70
|
-
</label>
|
|
71
|
-
<label class="prop-row"><span>Width</span>
|
|
72
|
-
<div class="prop-value"><input type="number" min="1" [ngModel]="e.w" (ngModelChange)="num($event, setW)" /></div>
|
|
73
|
-
</label>
|
|
74
|
-
<label class="prop-row"><span>Height</span>
|
|
75
|
-
<div class="prop-value">
|
|
76
|
-
@if (isTable()) {
|
|
77
|
-
<span class="muted">{{ e.h }} (auto)</span>
|
|
78
|
-
} @else {
|
|
79
|
-
<input type="number" min="1" [ngModel]="e.h" (ngModelChange)="num($event, setH)" />
|
|
80
|
-
}
|
|
81
|
-
</div>
|
|
82
|
-
</label>
|
|
83
|
-
</div>
|
|
84
|
-
@if (isText() || isTable()) {
|
|
85
|
-
<label class="check" title="Grow taller to fit long text; elements below are pushed down">
|
|
86
|
-
<input type="checkbox" [ngModel]="e.canGrow !== false" (ngModelChange)="set(setGrow($event))" />
|
|
87
|
-
Can grow (fit long text)
|
|
88
|
-
</label>
|
|
89
|
-
}
|
|
90
|
-
</section>
|
|
91
|
-
|
|
92
|
-
@if (isTable()) {
|
|
93
|
-
<rpt-table-props [el]="e" />
|
|
94
|
-
}
|
|
95
|
-
@switch (e.kind) {
|
|
96
|
-
@case ('chart') { @if (e.chart) { <rpt-chart-props [el]="e" /> } }
|
|
97
|
-
@case ('barcode') { @if (e.barcode) { <rpt-barcode-props [el]="e" /> } }
|
|
98
|
-
@case ('qrcode') { @if (e.qr) { <rpt-qr-props [el]="e" /> } }
|
|
99
|
-
@case ('crosstab') { @if (e.crosstab) { <rpt-crosstab-props [el]="e" /> } }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@if (isText()) {
|
|
103
|
-
<section>
|
|
104
|
-
<h4>Format</h4>
|
|
105
|
-
<label class="prop-row"><span>Type</span>
|
|
106
|
-
<div class="prop-value">
|
|
107
|
-
<select [ngModel]="e.format.kind" (ngModelChange)="set(setFormatKind($event))">
|
|
108
|
-
@for (k of formatKinds; track k) {
|
|
109
|
-
<option [value]="k">{{ k }}</option>
|
|
110
|
-
}
|
|
111
|
-
</select>
|
|
112
|
-
</div>
|
|
113
|
-
</label>
|
|
114
|
-
@if (numericFormats.includes(e.format.kind)) {
|
|
115
|
-
<label class="prop-row"><span>Decimals</span>
|
|
116
|
-
<div class="prop-value"><input type="number" min="0" [ngModel]="e.format.decimals" (ngModelChange)="num($event, setDecimals)" /></div>
|
|
117
|
-
</label>
|
|
118
|
-
}
|
|
119
|
-
@if (e.format.kind === 'currency') {
|
|
120
|
-
<label class="prop-row"><span>Currency</span>
|
|
121
|
-
<div class="prop-value"><input maxlength="3" [ngModel]="e.format.currency" (ngModelChange)="set(setCurrency($event))" /></div>
|
|
122
|
-
</label>
|
|
123
|
-
}
|
|
124
|
-
</section>
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
@if (isText()) {
|
|
128
|
-
@let o = options();
|
|
129
|
-
<section>
|
|
130
|
-
<h4>Field options</h4>
|
|
131
|
-
<div class="prop-label">Text when empty</div>
|
|
132
|
-
<rpt-expr-input [value]="o.nullText" placeholder="e.g. — or n/a" (valueChange)="setNullText($event)" />
|
|
133
|
-
<label class="check" title="Leave the box empty when the value is the same as in the row above (restarts on each page)">
|
|
134
|
-
<input type="checkbox" [ngModel]="o.hideDuplicates" (ngModelChange)="setHideDuplicates($event)" /> Hide repeated values
|
|
135
|
-
</label>
|
|
136
|
-
<div class="prop-label">Conditional formatting <span class="muted">— first matching rule wins</span></div>
|
|
137
|
-
@for (r of o.rules; track $index; let i = $index) {
|
|
138
|
-
<div class="col-card">
|
|
139
|
-
<div class="col-head">
|
|
140
|
-
<strong>When</strong><span class="spacer"></span>
|
|
141
|
-
<button class="small danger" title="Remove rule" (click)="removeRule(i)">✕</button>
|
|
142
|
-
</div>
|
|
143
|
-
<rpt-expr-input mode="expression" [value]="r.when" placeholder="e.g. Amount > 1000" (valueChange)="setRule(i, 'when', $event)" />
|
|
144
|
-
<div class="prop-row"><span>Text color</span>
|
|
145
|
-
<div class="prop-value">
|
|
146
|
-
<input type="color" [ngModel]="r.color" (ngModelChange)="setRule(i, 'color', $event)" />
|
|
147
|
-
<label class="check"><input type="checkbox" [ngModel]="r.bold" (ngModelChange)="setRule(i, 'bold', $event)" /> Bold</label>
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="prop-row"><span>Fill</span>
|
|
151
|
-
<div class="prop-value">
|
|
152
|
-
<input type="color" [ngModel]="r.background || '#ffffff'" (ngModelChange)="setRule(i, 'background', $event)" />
|
|
153
|
-
<button class="small" [class.on]="!r.background" (click)="setRule(i, 'background', '')">None</button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
}
|
|
158
|
-
<button (click)="addRule()">+ Add rule</button>
|
|
159
|
-
</section>
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
<section>
|
|
163
|
-
<h4>Visibility</h4>
|
|
164
|
-
<div class="prop-label">Visible when <span class="muted">— empty = always</span></div>
|
|
165
|
-
<rpt-expr-input mode="expression" [value]="e.visibleWhen ?? ''" placeholder="e.g. Amount > 0 or @ShowDetails" (valueChange)="set(setVisible($event))" />
|
|
166
|
-
</section>
|
|
167
|
-
|
|
168
|
-
@if (isText() || isTable() || hasFont()) {
|
|
169
|
-
<section>
|
|
170
|
-
<h4>Font</h4>
|
|
171
|
-
<label class="prop-row"><span>Family</span>
|
|
172
|
-
<div class="prop-value">
|
|
173
|
-
<select [ngModel]="s.fontFamily" (ngModelChange)="style('fontFamily', $event)">
|
|
174
|
-
<option value="Segoe UI, Arial, sans-serif">Sans-serif</option>
|
|
175
|
-
<option value="Georgia, 'Times New Roman', serif">Serif</option>
|
|
176
|
-
<option value="Consolas, 'Courier New', monospace">Monospace</option>
|
|
177
|
-
<option value="Tahoma, Arial, sans-serif">Tahoma (Arabic)</option>
|
|
178
|
-
</select>
|
|
179
|
-
</div>
|
|
180
|
-
</label>
|
|
181
|
-
<label class="prop-row"><span>Size</span>
|
|
182
|
-
<div class="prop-value"><input type="number" min="4" [ngModel]="s.fontSize" (ngModelChange)="numStyle('fontSize', $event)" /></div>
|
|
183
|
-
</label>
|
|
184
|
-
<div class="btn-group">
|
|
185
|
-
<button [class.on]="s.bold" (click)="style('bold', !s.bold)"><b>B</b></button>
|
|
186
|
-
<button [class.on]="s.italic" (click)="style('italic', !s.italic)"><i>I</i></button>
|
|
187
|
-
<button [class.on]="s.underline" (click)="style('underline', !s.underline)"><u>U</u></button>
|
|
188
|
-
@if (isText()) {
|
|
189
|
-
<span class="sep"></span>
|
|
190
|
-
<button [class.on]="s.align === 'left'" (click)="style('align', 'left')" title="Align left">⇤</button>
|
|
191
|
-
<button [class.on]="s.align === 'center'" (click)="style('align', 'center')" title="Align center">↔</button>
|
|
192
|
-
<button [class.on]="s.align === 'right'" (click)="style('align', 'right')" title="Align right">⇥</button>
|
|
193
|
-
}
|
|
194
|
-
<span class="sep"></span>
|
|
195
|
-
<button [class.on]="s.vAlign === 'top'" (click)="style('vAlign', 'top')" title="Vertical top">⤒</button>
|
|
196
|
-
<button [class.on]="s.vAlign === 'middle'" (click)="style('vAlign', 'middle')" title="Vertical middle">↕</button>
|
|
197
|
-
<button [class.on]="s.vAlign === 'bottom'" (click)="style('vAlign', 'bottom')" title="Vertical bottom">⤓</button>
|
|
198
|
-
</div>
|
|
199
|
-
<label class="prop-row"><span>Color</span>
|
|
200
|
-
<div class="prop-value"><input type="color" [ngModel]="s.color" (ngModelChange)="style('color', $event)" /></div>
|
|
201
|
-
</label>
|
|
202
|
-
<label class="prop-row"><span>Padding</span>
|
|
203
|
-
<div class="prop-value"><input type="number" min="0" [ngModel]="s.padding" (ngModelChange)="numStyle('padding', $event)" /></div>
|
|
204
|
-
</label>
|
|
205
|
-
</section>
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
@if (!isTable() && e.kind !== 'crosstab') {
|
|
209
|
-
<section>
|
|
210
|
-
<h4>{{ e.kind === 'line' ? 'Line' : 'Border & fill' }}</h4>
|
|
211
|
-
@if (e.kind !== 'line' && e.kind !== 'image') {
|
|
212
|
-
<div class="prop-row"><span>Background</span>
|
|
213
|
-
<div class="prop-value">
|
|
214
|
-
<input type="color" [ngModel]="s.background === 'transparent' ? '#ffffff' : s.background" (ngModelChange)="style('background', $event)" />
|
|
215
|
-
<button class="small" [class.on]="s.background === 'transparent'" (click)="style('background', 'transparent')">None</button>
|
|
216
|
-
</div>
|
|
217
|
-
</div>
|
|
218
|
-
}
|
|
219
|
-
@if (e.kind === 'line' || e.kind === 'rect') {
|
|
220
|
-
<label class="prop-row"><span>Style</span>
|
|
221
|
-
<div class="prop-value">
|
|
222
|
-
<select [ngModel]="e.dash ?? 'solid'" (ngModelChange)="set(setDash($event))">
|
|
223
|
-
<option value="solid">Solid ───</option>
|
|
224
|
-
<option value="dashed">Dashed - - -</option>
|
|
225
|
-
<option value="dotted">Dotted ·····</option>
|
|
226
|
-
</select>
|
|
227
|
-
</div>
|
|
228
|
-
</label>
|
|
229
|
-
}
|
|
230
|
-
<label class="prop-row"><span>{{ e.kind === 'line' ? 'Thickness' : 'Border width' }}</span>
|
|
231
|
-
<div class="prop-value"><input type="number" min="0" [ngModel]="s.borderWidth" (ngModelChange)="numStyle('borderWidth', $event)" /></div>
|
|
232
|
-
</label>
|
|
233
|
-
<label class="prop-row"><span>{{ e.kind === 'line' ? 'Color' : 'Border color' }}</span>
|
|
234
|
-
<div class="prop-value"><input type="color" [ngModel]="s.borderColor" (ngModelChange)="style('borderColor', $event)" /></div>
|
|
235
|
-
</label>
|
|
236
|
-
</section>
|
|
237
|
-
}
|
|
238
|
-
`,
|
|
239
|
-
})
|
|
240
|
-
export class ElementPropsComponent {
|
|
241
|
-
private readonly store = inject(DesignerStore);
|
|
242
|
-
readonly el = input.required<ReportElement>();
|
|
243
|
-
readonly count = input(1);
|
|
244
|
-
|
|
245
|
-
protected readonly formatKinds = FORMAT_KINDS;
|
|
246
|
-
protected readonly numericFormats = NUMERIC_FORMATS;
|
|
247
|
-
protected readonly aggregates = AGGREGATES.join(', ');
|
|
248
|
-
protected readonly isText = computed(() => this.el().kind === 'text' || this.el().kind === 'field');
|
|
249
|
-
protected readonly isTable = computed(() => this.el().kind === 'table' && !!this.el().table);
|
|
250
|
-
protected readonly isCode = computed(() => this.el().kind === 'barcode' || this.el().kind === 'qrcode');
|
|
251
|
-
/** Elements that draw text with the element font (beyond labels/fields/tables). */
|
|
252
|
-
protected readonly hasFont = computed(() => ['chart', 'crosstab', 'barcode'].includes(this.el().kind));
|
|
253
|
-
protected readonly title = computed(() => {
|
|
254
|
-
const k = this.el().kind;
|
|
255
|
-
return k === 'field' ? 'Field' : k[0].toUpperCase() + k.slice(1);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
private readonly width = computed(() => contentWidth(this.store.report().page));
|
|
259
|
-
|
|
260
|
-
// Setters (applied to every selected element).
|
|
261
|
-
protected readonly setX = (v: number) => (e: ReportElement) => (e.x = Math.max(0, Math.min(v, this.width() - e.w)));
|
|
262
|
-
protected readonly setY = (v: number) => (e: ReportElement) => (e.y = Math.max(0, v));
|
|
263
|
-
protected readonly setW = (v: number) => (e: ReportElement) => (e.w = Math.max(1, v));
|
|
264
|
-
protected readonly setH = (v: number) => (e: ReportElement) => (e.h = Math.max(1, v));
|
|
265
|
-
protected readonly setDecimals = (v: number) => (e: ReportElement) => (e.format.decimals = Math.max(0, Math.min(10, v)));
|
|
266
|
-
protected setText = (v: string) => (e: ReportElement) => (e.text = v);
|
|
267
|
-
protected setDash = (v: 'solid' | 'dashed' | 'dotted') => (e: ReportElement) => (e.dash = v === 'solid' ? undefined : v);
|
|
268
|
-
protected setFit = (v: 'contain' | 'cover' | 'fill') => (e: ReportElement) => (e.imageFit = v);
|
|
269
|
-
protected setVisible = (v: string) => (e: ReportElement) => (e.visibleWhen = v || undefined);
|
|
270
|
-
protected setGrow = (v: boolean) => (e: ReportElement) => (e.canGrow = v);
|
|
271
|
-
protected setFormatKind = (v: FormatKind) => (e: ReportElement) => (e.format.kind = v);
|
|
272
|
-
protected setCurrency = (v: string) => (e: ReportElement) => (e.format.currency = v.toUpperCase());
|
|
273
|
-
|
|
274
|
-
protected readonly options = computed(() => ({ ...defaultFieldOptions(), ...this.el().fieldOptions }));
|
|
275
|
-
|
|
276
|
-
protected setNullText(v: string): void {
|
|
277
|
-
this.set((e) => (e.fieldOptions = { ...defaultFieldOptions(), ...e.fieldOptions, nullText: v }));
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
protected setHideDuplicates(v: boolean): void {
|
|
281
|
-
this.set((e) => (e.fieldOptions = { ...defaultFieldOptions(), ...e.fieldOptions, hideDuplicates: v }));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
protected addRule(): void {
|
|
285
|
-
this.set((e) => {
|
|
286
|
-
const o = { ...defaultFieldOptions(), ...e.fieldOptions };
|
|
287
|
-
e.fieldOptions = { ...o, rules: [...o.rules, { when: '', color: '#c4372f', background: '', bold: true }] };
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
protected setRule<K extends keyof StyleRule>(i: number, k: K, v: StyleRule[K]): void {
|
|
292
|
-
this.set((e) => {
|
|
293
|
-
const r = e.fieldOptions?.rules[i];
|
|
294
|
-
if (r) r[k] = v;
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
protected removeRule(i: number): void {
|
|
299
|
-
this.set((e) => e.fieldOptions?.rules.splice(i, 1));
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
protected set(fn: (e: ReportElement) => void): void {
|
|
303
|
-
const ids = this.store.selection().elementIds;
|
|
304
|
-
this.store.update((r) => {
|
|
305
|
-
for (const b of r.bands)
|
|
306
|
-
for (const e of b.elements)
|
|
307
|
-
if (ids.includes(e.id)) {
|
|
308
|
-
fn(e);
|
|
309
|
-
// Keep the element inside its band: grow the band to fit a new Y/height.
|
|
310
|
-
b.height = Math.max(b.height, e.y + e.h);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
protected num(v: number | null, setter: (v: number) => (e: ReportElement) => void): void {
|
|
316
|
-
if (v != null && Number.isFinite(v)) this.set(setter(v));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
protected style<K extends keyof ElementStyle>(k: K, v: ElementStyle[K]): void {
|
|
320
|
-
this.set((e) => (e.style[k] = v));
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
protected numStyle(k: 'fontSize' | 'padding' | 'borderWidth', v: number | null): void {
|
|
324
|
-
if (v != null && Number.isFinite(v)) this.style(k, Math.max(0, v));
|
|
325
|
-
}
|
|
326
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, ElementRef, computed, inject, input, output, signal, viewChild } from '@angular/core';
|
|
2
|
-
import { FUNCTIONS, SPECIAL_FIELDS, checkExpression, checkTemplate } from '../../engine/expressions';
|
|
3
|
-
import { DesignerStore } from '../designer.store';
|
|
4
|
-
|
|
5
|
-
interface Suggestion {
|
|
6
|
-
label: string;
|
|
7
|
-
insert: string;
|
|
8
|
-
kind: 'field' | 'param' | 'fn' | 'special';
|
|
9
|
-
detail: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const KIND_ICON = { field: 'Ab', param: '@', fn: 'ƒ', special: '★' } as const;
|
|
13
|
-
const SIMPLE_NAME = /^[A-Za-z_À-][\w.À-]*$/;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Text box for templates ("Total: {sum(Amount)}") or bare expressions ("Amount > 100") with
|
|
17
|
-
* autocomplete of fields, @parameters, functions and special fields, plus live validation.
|
|
18
|
-
* Template mode suggests inside {…}; expression mode suggests anywhere. Ctrl+Space opens the list.
|
|
19
|
-
*/
|
|
20
|
-
@Component({
|
|
21
|
-
selector: 'rpt-expr-input',
|
|
22
|
-
standalone: true,
|
|
23
|
-
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
-
template: `
|
|
25
|
-
<div class="expr-input">
|
|
26
|
-
@if (rows() > 1) {
|
|
27
|
-
<textarea
|
|
28
|
-
#box
|
|
29
|
-
[rows]="rows()"
|
|
30
|
-
[value]="value()"
|
|
31
|
-
[placeholder]="placeholder()"
|
|
32
|
-
spellcheck="false"
|
|
33
|
-
(input)="onInput()"
|
|
34
|
-
(keydown)="onKey($event)"
|
|
35
|
-
(click)="refresh()"
|
|
36
|
-
(blur)="open.set(false)"
|
|
37
|
-
></textarea>
|
|
38
|
-
} @else {
|
|
39
|
-
<input
|
|
40
|
-
#box
|
|
41
|
-
[value]="value()"
|
|
42
|
-
[placeholder]="placeholder()"
|
|
43
|
-
spellcheck="false"
|
|
44
|
-
(input)="onInput()"
|
|
45
|
-
(keydown)="onKey($event)"
|
|
46
|
-
(click)="refresh()"
|
|
47
|
-
(blur)="open.set(false)"
|
|
48
|
-
/>
|
|
49
|
-
}
|
|
50
|
-
@if (open() && items().length) {
|
|
51
|
-
<ul class="ac-list" role="listbox">
|
|
52
|
-
@for (it of items(); track it.kind + it.label; let i = $index) {
|
|
53
|
-
<li [class.active]="i === active()" role="option" (mousedown)="$event.preventDefault(); accept(it)">
|
|
54
|
-
<span class="ac-kind ac-{{ it.kind }}">{{ icons[it.kind] }}</span>
|
|
55
|
-
<b>{{ it.label }}</b>
|
|
56
|
-
<span class="muted">{{ it.detail }}</span>
|
|
57
|
-
</li>
|
|
58
|
-
}
|
|
59
|
-
</ul>
|
|
60
|
-
}
|
|
61
|
-
@if (error()) {
|
|
62
|
-
<div class="expr-error">⚠ {{ error() }}</div>
|
|
63
|
-
}
|
|
64
|
-
</div>
|
|
65
|
-
`,
|
|
66
|
-
})
|
|
67
|
-
export class ExprInputComponent {
|
|
68
|
-
private readonly store = inject(DesignerStore);
|
|
69
|
-
readonly value = input('');
|
|
70
|
-
readonly mode = input<'template' | 'expression'>('template');
|
|
71
|
-
readonly rows = input(1);
|
|
72
|
-
readonly placeholder = input('');
|
|
73
|
-
readonly valueChange = output<string>();
|
|
74
|
-
|
|
75
|
-
private readonly box = viewChild<ElementRef<HTMLInputElement | HTMLTextAreaElement>>('box');
|
|
76
|
-
protected readonly icons = KIND_ICON;
|
|
77
|
-
protected readonly open = signal(false);
|
|
78
|
-
protected readonly active = signal(0);
|
|
79
|
-
protected readonly items = signal<Suggestion[]>([]);
|
|
80
|
-
/** The partial word being completed: [start, end) in the box. */
|
|
81
|
-
private word = { start: 0, end: 0 };
|
|
82
|
-
|
|
83
|
-
protected readonly error = computed(() => (this.mode() === 'template' ? checkTemplate(this.value()) : checkExpression(this.value())));
|
|
84
|
-
|
|
85
|
-
private readonly all = computed<Suggestion[]>(() => [
|
|
86
|
-
...this.store.data().fields.map((f) => ({
|
|
87
|
-
label: f.name,
|
|
88
|
-
insert: SIMPLE_NAME.test(f.name) ? f.name : `[${f.name}]`,
|
|
89
|
-
kind: 'field' as const,
|
|
90
|
-
detail: f.type,
|
|
91
|
-
})),
|
|
92
|
-
...(this.store.data().docFields ?? []).map((f) => ({ label: f.name, insert: f.name, kind: 'field' as const, detail: `document · ${f.type}` })),
|
|
93
|
-
...(this.store.report().parameters ?? [])
|
|
94
|
-
.filter((p) => p.name)
|
|
95
|
-
.map((p) => ({ label: '@' + p.name, insert: '@' + p.name, kind: 'param' as const, detail: `parameter · ${p.type}` })),
|
|
96
|
-
...FUNCTIONS.map((f) => ({ label: f.name + '()', insert: f.name, kind: 'fn' as const, detail: f.signature })),
|
|
97
|
-
...SPECIAL_FIELDS.map((s) => ({ label: s, insert: s, kind: 'special' as const, detail: 'special field' })),
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
protected onInput(): void {
|
|
101
|
-
const el = this.box()?.nativeElement;
|
|
102
|
-
if (!el) return;
|
|
103
|
-
this.valueChange.emit(el.value);
|
|
104
|
-
this.refresh();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Works out whether the caret is inside an expression and which word is being typed. */
|
|
108
|
-
protected refresh(force = false): void {
|
|
109
|
-
const el = this.box()?.nativeElement;
|
|
110
|
-
if (!el) return;
|
|
111
|
-
const pos = el.selectionStart ?? el.value.length;
|
|
112
|
-
const before = el.value.slice(0, pos);
|
|
113
|
-
let start = 0;
|
|
114
|
-
if (this.mode() === 'template') {
|
|
115
|
-
const openAt = before.lastIndexOf('{');
|
|
116
|
-
if (openAt < 0 || before.lastIndexOf('}') > openAt) return this.open.set(false);
|
|
117
|
-
start = openAt + 1;
|
|
118
|
-
}
|
|
119
|
-
const seg = before.slice(start);
|
|
120
|
-
if ((seg.match(/"/g) ?? []).length % 2 === 1 || (seg.match(/'/g) ?? []).length % 2 === 1) return this.open.set(false); // inside a string
|
|
121
|
-
const word = /@?\[?[\w.À-]*$/.exec(seg)?.[0] ?? '';
|
|
122
|
-
this.word = { start: pos - word.length, end: pos };
|
|
123
|
-
|
|
124
|
-
const q = word.replace(/^[@[]/, '').toLowerCase();
|
|
125
|
-
let pool = this.all();
|
|
126
|
-
if (word.startsWith('@')) pool = pool.filter((s) => s.kind === 'param');
|
|
127
|
-
const matches = q
|
|
128
|
-
? [...pool.filter((s) => s.label.replace(/^@/, '').toLowerCase().startsWith(q)), ...pool.filter((s) => !s.label.replace(/^@/, '').toLowerCase().startsWith(q) && s.label.toLowerCase().includes(q))]
|
|
129
|
-
: pool;
|
|
130
|
-
// Don't pop up for a finished word that exactly matches, unless asked.
|
|
131
|
-
const exact = matches.length === 1 && matches[0].insert.toLowerCase() === word.toLowerCase();
|
|
132
|
-
this.items.set(matches.slice(0, 40));
|
|
133
|
-
this.active.set(0);
|
|
134
|
-
this.open.set((force || word.length > 0 || /[{(,\s+\-*/<>=!?:&|]$/.test(before) || seg === '') && matches.length > 0 && !exact);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
protected onKey(e: KeyboardEvent): void {
|
|
138
|
-
// Typing "}" right before an auto-inserted "}" steps over it instead of doubling it.
|
|
139
|
-
const el = this.box()?.nativeElement;
|
|
140
|
-
if (e.key === '}' && el && el.selectionStart === el.selectionEnd && el.value[el.selectionStart ?? -1] === '}') {
|
|
141
|
-
e.preventDefault();
|
|
142
|
-
const p = (el.selectionStart ?? 0) + 1;
|
|
143
|
-
el.setSelectionRange(p, p);
|
|
144
|
-
this.open.set(false);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
if (e.key === ' ' && e.ctrlKey) {
|
|
148
|
-
e.preventDefault();
|
|
149
|
-
this.refresh(true);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (!this.open() || !this.items().length) return;
|
|
153
|
-
const n = this.items().length;
|
|
154
|
-
if (e.key === 'ArrowDown') {
|
|
155
|
-
e.preventDefault();
|
|
156
|
-
this.active.set((this.active() + 1) % n);
|
|
157
|
-
} else if (e.key === 'ArrowUp') {
|
|
158
|
-
e.preventDefault();
|
|
159
|
-
this.active.set((this.active() - 1 + n) % n);
|
|
160
|
-
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
|
161
|
-
e.preventDefault();
|
|
162
|
-
this.accept(this.items()[this.active()]);
|
|
163
|
-
} else if (e.key === 'Escape') {
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
e.stopPropagation();
|
|
166
|
-
this.open.set(false);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Inserts the suggestion; functions get "()" with the caret inside, templates get a closing "}". */
|
|
171
|
-
protected accept(s: Suggestion): void {
|
|
172
|
-
const el = this.box()?.nativeElement;
|
|
173
|
-
if (!el) return;
|
|
174
|
-
const v = el.value;
|
|
175
|
-
const after = v.slice(this.word.end);
|
|
176
|
-
let insert = s.kind === 'fn' ? s.insert + '()' : s.insert;
|
|
177
|
-
let caret = this.word.start + (s.kind === 'fn' ? s.insert.length + 1 : insert.length);
|
|
178
|
-
if (this.mode() === 'template' && s.kind !== 'fn') {
|
|
179
|
-
const nextClose = after.indexOf('}');
|
|
180
|
-
const nextOpen = after.indexOf('{');
|
|
181
|
-
// Close the expression, but keep the caret inside so the formula can continue ("{Qty * …").
|
|
182
|
-
if (nextClose < 0 || (nextOpen >= 0 && nextOpen < nextClose)) insert += '}';
|
|
183
|
-
}
|
|
184
|
-
el.value = v.slice(0, this.word.start) + insert + after;
|
|
185
|
-
el.setSelectionRange(caret, caret);
|
|
186
|
-
el.focus();
|
|
187
|
-
this.valueChange.emit(el.value);
|
|
188
|
-
this.open.set(false);
|
|
189
|
-
if (s.kind === 'fn') this.refresh(true); // suggest the argument
|
|
190
|
-
}
|
|
191
|
-
}
|