@valtimo/components 13.32.0 → 13.34.0

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.
@@ -4498,6 +4498,10 @@ class OverflowMenuComponent {
4498
4498
  return;
4499
4499
  this._cleanupAutoUpdate = autoUpdate(reference, menu, () => {
4500
4500
  computePosition(reference, menu, {
4501
+ // The pane is rendered with `position: fixed`, so positions must be
4502
+ // computed against the viewport. Without this the menu is offset by the
4503
+ // trigger's page position and lands far from lower rows (unclickable).
4504
+ strategy: 'fixed',
4501
4505
  placement: this.placement,
4502
4506
  middleware: [
4503
4507
  offset({ mainAxis: this.offsetY, crossAxis: this.offsetX }),
@@ -8996,6 +9000,9 @@ class FormioComponent {
8996
9000
  set readOnly(readOnlyValue) {
8997
9001
  this.readOnly$.next(readOnlyValue);
8998
9002
  }
9003
+ set errors(errorsValue) {
9004
+ this.errors$.next(errorsValue ?? []);
9005
+ }
8999
9006
  handleBeforeUnload() {
9000
9007
  this.localStorageService.clearTokenFromLocalStorage();
9001
9008
  }
@@ -9164,7 +9171,7 @@ class FormioComponent {
9164
9171
  this._overrideOptions$.next(config.formioOptions);
9165
9172
  }
9166
9173
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FormioComponent, deps: [{ token: i6$1.UserProviderService }, { token: i4.NGXLogger }, { token: FormIoStateService }, { token: i1$3.ActivatedRoute }, { token: i1.TranslateService }, { token: FormIoLocalStorageService }, { token: ValtimoModalService }, { token: i1$2.ConfigService }, { token: FormIoTagsService }, { token: i0.Injector }, { token: FormioTranslationService }], target: i0.ɵɵFactoryTarget.Component }); }
9167
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: FormioComponent, isStandalone: false, selector: "valtimo-form-io", inputs: { options: "options", submission: "submission", form: "form", readOnly: "readOnly", formRefresh$: "formRefresh$" }, outputs: { submit: "submit", change: "change", event: "event" }, host: { listeners: { "window:beforeunload": "handleBeforeUnload($event)" } }, providers: [FormIoLocalStorageService], usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div\n class=\"formio-carbon\"\n cdsLayer=\"1\"\n *ngIf=\"{\n currentLanguage: currentLanguage$ | async,\n formioOptions: formioOptions$ | async,\n submission: submission$ | async,\n readOnly: readOnly$ | async,\n form: form$ | async,\n errors: errors$ | async,\n tokenSetInLocalStorage: tokenSetInLocalStorage$ | async,\n } as obs\"\n>\n <div *ngIf=\"obs.errors.length > 0\" class=\"alert alert-danger pt-5 pb-5 mb-2\">\n <ol>\n <li *ngFor=\"let error of obs.errors\">\n {{ error }}\n </li>\n </ol>\n </div>\n\n <formio\n *ngIf=\"obs.formioOptions && obs.form && obs.tokenSetInLocalStorage\"\n [form]=\"obs.form\"\n [language]=\"languageEventEmitter\"\n [options]=\"obs.formioOptions\"\n [readOnly]=\"obs.readOnly\"\n [refresh]=\"refreshForm\"\n [submission]=\"obs.submission\"\n (change)=\"onChange($event)\"\n (nextPage)=\"nextPage()\"\n (prevPage)=\"prevPage()\"\n (ready)=\"formReady($event)\"\n (submit)=\"onSubmit($event)\"\n (customEvent)=\"onCustomEvent($event)\"\n ></formio>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i9.FormioComponent, selector: "formio" }, { kind: "directive", type: i2$3.LayerDirective, selector: "[cdsLayer], [ibmLayer]", inputs: ["ibmLayer", "cdsLayer"], exportAs: ["layer"] }, { kind: "pipe", type: i1$4.AsyncPipe, name: "async" }] }); }
9174
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: FormioComponent, isStandalone: false, selector: "valtimo-form-io", inputs: { options: "options", submission: "submission", form: "form", readOnly: "readOnly", errors: "errors", formRefresh$: "formRefresh$" }, outputs: { submit: "submit", change: "change", event: "event" }, host: { listeners: { "window:beforeunload": "handleBeforeUnload($event)" } }, providers: [FormIoLocalStorageService], usesOnChanges: true, ngImport: i0, template: "<!--\n ~ Copyright 2015-2025 Ritense BV, the Netherlands.\n ~\n ~ Licensed under EUPL, Version 1.2 (the \"License\");\n ~ you may not use this file except in compliance with the License.\n ~ You may obtain a copy of the License at\n ~\n ~ https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12\n ~\n ~ Unless required by applicable law or agreed to in writing, software\n ~ distributed under the License is distributed on an \"AS IS\" basis,\n ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n ~ See the License for the specific language governing permissions and\n ~ limitations under the License.\n -->\n\n<div\n class=\"formio-carbon\"\n cdsLayer=\"1\"\n *ngIf=\"{\n currentLanguage: currentLanguage$ | async,\n formioOptions: formioOptions$ | async,\n submission: submission$ | async,\n readOnly: readOnly$ | async,\n form: form$ | async,\n errors: errors$ | async,\n tokenSetInLocalStorage: tokenSetInLocalStorage$ | async,\n } as obs\"\n>\n <div *ngIf=\"obs.errors.length > 0\" class=\"alert alert-danger pt-5 pb-5 mb-2\">\n <ol>\n <li *ngFor=\"let error of obs.errors\">\n {{ error }}\n </li>\n </ol>\n </div>\n\n <formio\n *ngIf=\"obs.formioOptions && obs.form && obs.tokenSetInLocalStorage\"\n [form]=\"obs.form\"\n [language]=\"languageEventEmitter\"\n [options]=\"obs.formioOptions\"\n [readOnly]=\"obs.readOnly\"\n [refresh]=\"refreshForm\"\n [submission]=\"obs.submission\"\n (change)=\"onChange($event)\"\n (nextPage)=\"nextPage()\"\n (prevPage)=\"prevPage()\"\n (ready)=\"formReady($event)\"\n (submit)=\"onSubmit($event)\"\n (customEvent)=\"onCustomEvent($event)\"\n ></formio>\n</div>\n", styles: [""], dependencies: [{ kind: "directive", type: i1$4.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$4.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i9.FormioComponent, selector: "formio" }, { kind: "directive", type: i2$3.LayerDirective, selector: "[cdsLayer], [ibmLayer]", inputs: ["ibmLayer", "cdsLayer"], exportAs: ["layer"] }, { kind: "pipe", type: i1$4.AsyncPipe, name: "async" }] }); }
9168
9175
  }
9169
9176
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: FormioComponent, decorators: [{
9170
9177
  type: Component,
@@ -9177,6 +9184,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
9177
9184
  type: Input
9178
9185
  }], readOnly: [{
9179
9186
  type: Input
9187
+ }], errors: [{
9188
+ type: Input
9180
9189
  }], formRefresh$: [{
9181
9190
  type: Input
9182
9191
  }], submit: [{
@@ -17756,6 +17765,421 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
17756
17765
  type: Input
17757
17766
  }] } });
17758
17767
 
17768
+ /*
17769
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
17770
+ *
17771
+ * Licensed under EUPL, Version 1.2 (the "License");
17772
+ * you may not use this file except in compliance with the License.
17773
+ * You may obtain a copy of the License at
17774
+ *
17775
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
17776
+ *
17777
+ * Unless required by applicable law or agreed to in writing, software
17778
+ * distributed under the License is distributed on an "AS IS" basis,
17779
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17780
+ * See the License for the specific language governing permissions and
17781
+ * limitations under the License.
17782
+ */
17783
+ /**
17784
+ * Custom Muuri layout function — "beautiful" gap-free packing.
17785
+ *
17786
+ * Each widget occupies a whole number of grid columns (1-4) and a whole
17787
+ * number of row units (height scales with content). The grid column count is
17788
+ * derived from the actual item dimensions so the algorithm works identically
17789
+ * for case widgets (320px / 200px) and dashboard widgets (275px / 220px)
17790
+ * without hardcoding either constant.
17791
+ *
17792
+ * Goal (in priority order):
17793
+ * 1. Never leave a gap trapped in the middle (a hole with a widget below it).
17794
+ * 2. Keep the block compact (minimise height) and push any unavoidable
17795
+ * gaps to the bottom-right corner.
17796
+ * 3. Respect the source order as much as the above two allow.
17797
+ *
17798
+ * To achieve this we treat each section as a small 2D strip-packing problem
17799
+ * and search:
17800
+ * - over the number of columns to actually use (sometimes a narrower block
17801
+ * packs into a cleaner rectangle than spreading across every column);
17802
+ * - over widget orderings (reordering is allowed to fill gaps).
17803
+ * Every candidate layout is scored and the prettiest one wins. Because the
17804
+ * packing only depends on the column/row spans and the column count (not the
17805
+ * exact pixel width) results are memoised and reused across resizes.
17806
+ *
17807
+ * Compatible with Muuri's custom layout function API (v0.9.x):
17808
+ * layout(grid, layoutId, items, gridWidth, gridHeight, callback)
17809
+ */
17810
+ /** Maximum number of grid columns to consider when detecting the column count. */
17811
+ const MAX_COLS = 20;
17812
+ /** Tolerance (in pixels) for floating-point width/height comparisons. */
17813
+ const SNAP_PX = 1;
17814
+ /**
17815
+ * How far (as a fraction of one column) an item width may deviate from a whole
17816
+ * number of columns and still be considered an integer span. Loose enough to
17817
+ * absorb sub-pixel column widths and widget margins, tight enough to reject a
17818
+ * grid that does not actually fit the widths.
17819
+ */
17820
+ const COL_SNAP_RATIO = 0.12;
17821
+ /** Above this item count we stop trying every permutation and use heuristics. */
17822
+ const FULL_PERMUTATION_LIMIT = 7;
17823
+ /** Scoring weights — tuned so each rule dominates the ones below it. */
17824
+ const W_INTERNAL_GAP = 100000; // a hole with a widget below it: avoid at all costs
17825
+ const W_BOTTOM_GAP = 1000; // an empty cell inside the full-width bounding box
17826
+ const W_HEIGHT = 50; // prefer a compact (shorter) block
17827
+ const W_GAP_NOT_RIGHT = 80; // push the bottom gaps towards the right
17828
+ const W_INVERSION = 5; // respect the original order when everything else ties
17829
+ /** Memoised packings keyed by span signature + column count. */
17830
+ const layoutCache = new Map();
17831
+ const LAYOUT_CACHE_MAX = 256;
17832
+ function muuriGapFreeLayout(grid, layoutId, items, gridWidth, _gridHeight, callback) {
17833
+ const slots = new Float32Array(items.length * 2);
17834
+ if (items.length === 0) {
17835
+ callback({ id: layoutId, items, slots, width: gridWidth, height: 0, styles: { height: '0px' } });
17836
+ return;
17837
+ }
17838
+ // Pre-compute outer dimensions (pixels).
17839
+ const pxWidths = [];
17840
+ const pxHeights = [];
17841
+ for (let i = 0; i < items.length; i++) {
17842
+ const item = items[i];
17843
+ pxWidths.push(item._width + item._marginLeft + item._marginRight);
17844
+ pxHeights.push(item._height + item._marginTop + item._marginBottom);
17845
+ }
17846
+ // Detect the column count and row unit from the item dimensions.
17847
+ const totalCols = detectTotalCols(pxWidths, gridWidth);
17848
+ const colWidth = gridWidth / totalCols;
17849
+ const rowHeight = detectRowHeight(pxHeights);
17850
+ // Convert each item to column/row units.
17851
+ const colSpans = [];
17852
+ const rowSpans = [];
17853
+ for (let i = 0; i < items.length; i++) {
17854
+ colSpans.push(Math.min(totalCols, Math.max(1, Math.round(pxWidths[i] / colWidth))));
17855
+ rowSpans.push(Math.max(1, Math.round(pxHeights[i] / rowHeight)));
17856
+ }
17857
+ const placement = packSection(colSpans, rowSpans, totalCols);
17858
+ // Convert grid positions to pixel coordinates.
17859
+ for (let i = 0; i < items.length; i++) {
17860
+ slots[i * 2] = Math.round(placement.cols[i] * colWidth);
17861
+ slots[i * 2 + 1] = Math.round(placement.rows[i] * rowHeight);
17862
+ }
17863
+ const layoutHeight = Math.round(placement.height * rowHeight);
17864
+ const el = grid._element;
17865
+ const isBorderBox = el ? getComputedStyle(el).boxSizing === 'border-box' : false;
17866
+ const borderTop = grid._borderTop || 0;
17867
+ const borderBottom = grid._borderBottom || 0;
17868
+ const totalHeight = isBorderBox ? layoutHeight + borderTop + borderBottom : layoutHeight;
17869
+ callback({
17870
+ id: layoutId,
17871
+ items,
17872
+ slots,
17873
+ styles: { height: totalHeight + 'px' },
17874
+ width: gridWidth,
17875
+ height: layoutHeight,
17876
+ });
17877
+ }
17878
+ /**
17879
+ * Pack one section. Searches over the number of columns to use and over item
17880
+ * orderings, scoring every candidate, and returns the prettiest placement.
17881
+ */
17882
+ function packSection(colSpans, rowSpans, maxCols) {
17883
+ const key = `${colSpans.join(',')}|${rowSpans.join(',')}|${maxCols}`;
17884
+ const cached = layoutCache.get(key);
17885
+ if (cached)
17886
+ return cached;
17887
+ const n = colSpans.length;
17888
+ let best = null;
17889
+ let bestScore = Infinity;
17890
+ for (let k = maxCols; k >= 1; k--) {
17891
+ // Widgets wider than the current column count become full-width.
17892
+ const cs = colSpans.map(c => Math.min(c, k));
17893
+ const orders = generateOrders(n, cs, rowSpans);
17894
+ for (const order of orders) {
17895
+ const placed = firstFitPack(order, cs, rowSpans, k);
17896
+ const score = scorePlacement(placed, cs, rowSpans, maxCols);
17897
+ if (score < bestScore) {
17898
+ bestScore = score;
17899
+ best = placed;
17900
+ }
17901
+ }
17902
+ }
17903
+ const result = best;
17904
+ if (layoutCache.size >= LAYOUT_CACHE_MAX)
17905
+ layoutCache.clear();
17906
+ layoutCache.set(key, result);
17907
+ return result;
17908
+ }
17909
+ /**
17910
+ * Place items (in the given order) into a `k`-column grid using top-left-first
17911
+ * first-fit. Returns the column/row of each item indexed by its original index.
17912
+ */
17913
+ function firstFitPack(order, colSpans, rowSpans, k) {
17914
+ const occupied = [];
17915
+ let gridRows = 0;
17916
+ const cols = new Array(colSpans.length);
17917
+ const rows = new Array(colSpans.length);
17918
+ const ensureRows = (needed) => {
17919
+ while (gridRows < needed) {
17920
+ occupied.push(new Array(k).fill(false));
17921
+ gridRows++;
17922
+ }
17923
+ };
17924
+ for (const idx of order) {
17925
+ const cw = colSpans[idx];
17926
+ const rh = rowSpans[idx];
17927
+ let placed = false;
17928
+ for (let r = 0; !placed; r++) {
17929
+ ensureRows(r + rh);
17930
+ for (let c = 0; c <= k - cw; c++) {
17931
+ if (canPlace(occupied, r, c, rh, cw)) {
17932
+ markOccupied(occupied, r, c, rh, cw);
17933
+ cols[idx] = c;
17934
+ rows[idx] = r;
17935
+ placed = true;
17936
+ break;
17937
+ }
17938
+ }
17939
+ }
17940
+ }
17941
+ let height = 0;
17942
+ for (let i = 0; i < colSpans.length; i++) {
17943
+ height = Math.max(height, rows[i] + rowSpans[i]);
17944
+ }
17945
+ return { cols, rows, height };
17946
+ }
17947
+ /**
17948
+ * Score a placement — lower is prettier. Encodes the priority order:
17949
+ * no trapped gaps > compact (short) block > gaps bottom-right > original order.
17950
+ *
17951
+ * Waste is always measured against the full available width (`maxCols`), so a
17952
+ * tall narrow tower that leaves whole columns empty on the right is correctly
17953
+ * seen as gappy rather than "gap free". A narrower block is therefore only
17954
+ * chosen when it does not add height and produces a cleaner gap placement.
17955
+ */
17956
+ function scorePlacement(placed, colSpans, rowSpans, maxCols) {
17957
+ const { cols, rows, height } = placed;
17958
+ const n = colSpans.length;
17959
+ // Build the occupancy grid across the full available width.
17960
+ const occ = [];
17961
+ for (let r = 0; r < height; r++)
17962
+ occ.push(new Array(maxCols).fill(false));
17963
+ for (let i = 0; i < n; i++) {
17964
+ for (let r = rows[i]; r < rows[i] + rowSpans[i]; r++) {
17965
+ for (let c = cols[i]; c < cols[i] + colSpans[i]; c++)
17966
+ occ[r][c] = true;
17967
+ }
17968
+ }
17969
+ let internalGaps = 0;
17970
+ let bottomGaps = 0;
17971
+ let gapNotRight = 0;
17972
+ for (let c = 0; c < maxCols; c++) {
17973
+ let lastFilled = -1;
17974
+ for (let r = 0; r < height; r++)
17975
+ if (occ[r][c])
17976
+ lastFilled = r;
17977
+ for (let r = 0; r < height; r++) {
17978
+ if (occ[r][c])
17979
+ continue;
17980
+ if (r < lastFilled) {
17981
+ internalGaps++;
17982
+ }
17983
+ else {
17984
+ bottomGaps++;
17985
+ gapNotRight += maxCols - 1 - c;
17986
+ }
17987
+ }
17988
+ }
17989
+ const inversions = countInversions(cols, rows, n);
17990
+ return (internalGaps * W_INTERNAL_GAP +
17991
+ bottomGaps * W_BOTTOM_GAP +
17992
+ height * W_HEIGHT +
17993
+ gapNotRight * W_GAP_NOT_RIGHT +
17994
+ inversions * W_INVERSION);
17995
+ }
17996
+ /**
17997
+ * Number of pairs whose reading order (top-to-bottom, left-to-right) differs
17998
+ * from their original order. Used as a tie-breaker to respect source order.
17999
+ */
18000
+ function countInversions(cols, rows, n) {
18001
+ const order = Array.from({ length: n }, (_, i) => i).sort((a, b) => rows[a] !== rows[b] ? rows[a] - rows[b] : cols[a] - cols[b]);
18002
+ const rank = new Array(n);
18003
+ for (let pos = 0; pos < n; pos++)
18004
+ rank[order[pos]] = pos;
18005
+ let inversions = 0;
18006
+ for (let i = 0; i < n; i++) {
18007
+ for (let j = i + 1; j < n; j++)
18008
+ if (rank[i] > rank[j])
18009
+ inversions++;
18010
+ }
18011
+ return inversions;
18012
+ }
18013
+ /**
18014
+ * Candidate orderings to try. For small sections every permutation is tried;
18015
+ * for larger sections a handful of size-based heuristics keeps it fast.
18016
+ */
18017
+ function generateOrders(n, colSpans, rowSpans) {
18018
+ const identity = Array.from({ length: n }, (_, i) => i);
18019
+ if (n <= 1)
18020
+ return [identity];
18021
+ if (n <= FULL_PERMUTATION_LIMIT)
18022
+ return permutations(identity);
18023
+ const byArea = [...identity].sort((a, b) => colSpans[b] * rowSpans[b] - colSpans[a] * rowSpans[a]);
18024
+ const byHeight = [...identity].sort((a, b) => rowSpans[b] - rowSpans[a] || colSpans[b] - colSpans[a]);
18025
+ const byWidth = [...identity].sort((a, b) => colSpans[b] - colSpans[a] || rowSpans[b] - rowSpans[a]);
18026
+ return dedupeOrders([identity, byArea, byHeight, byWidth]);
18027
+ }
18028
+ /** All permutations of the given index array (used only for small sections). */
18029
+ function permutations(arr) {
18030
+ if (arr.length <= 1)
18031
+ return [arr];
18032
+ const result = [];
18033
+ for (let i = 0; i < arr.length; i++) {
18034
+ const rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
18035
+ for (const perm of permutations(rest))
18036
+ result.push([arr[i], ...perm]);
18037
+ }
18038
+ return result;
18039
+ }
18040
+ function dedupeOrders(orders) {
18041
+ const seen = new Set();
18042
+ const out = [];
18043
+ for (const o of orders) {
18044
+ const key = o.join(',');
18045
+ if (!seen.has(key)) {
18046
+ seen.add(key);
18047
+ out.push(o);
18048
+ }
18049
+ }
18050
+ return out;
18051
+ }
18052
+ /**
18053
+ * Detect the number of grid columns: the *coarsest* grid (smallest column
18054
+ * count, from 1 up to MAX_COLS) in which every item width is approximately a
18055
+ * whole number of columns. A coarser grid is always preferred — a finer grid
18056
+ * that merely happens to also divide the widths (e.g. describing 1x/2x widgets
18057
+ * as 6/12 columns) yields the exact same pixel layout but destabilises the
18058
+ * column spans and the packing search.
18059
+ */
18060
+ function detectTotalCols(pxWidths, gridWidth) {
18061
+ for (let n = 1; n <= MAX_COLS; n++) {
18062
+ const unit = gridWidth / n;
18063
+ let valid = true;
18064
+ for (const w of pxWidths) {
18065
+ if (w < SNAP_PX)
18066
+ continue;
18067
+ const ratio = w / unit;
18068
+ if (Math.round(ratio) < 1 || Math.abs(ratio - Math.round(ratio)) > COL_SNAP_RATIO) {
18069
+ valid = false;
18070
+ break;
18071
+ }
18072
+ }
18073
+ if (valid)
18074
+ return n;
18075
+ }
18076
+ return 1;
18077
+ }
18078
+ /**
18079
+ * Detect the base row unit as the greatest common divisor of the item heights.
18080
+ * Heights are exact integer multiples of the base unit (e.g. 200 for case
18081
+ * widgets, 220 for dashboard widgets); the GCD recovers that base even when a
18082
+ * section happens to contain no single-row-tall widget.
18083
+ */
18084
+ function detectRowHeight(pxHeights) {
18085
+ let unit = 0;
18086
+ for (const h of pxHeights) {
18087
+ const rounded = Math.round(h);
18088
+ if (rounded > SNAP_PX)
18089
+ unit = gcd(unit, rounded);
18090
+ }
18091
+ return unit > SNAP_PX ? unit : 200;
18092
+ }
18093
+ function gcd(a, b) {
18094
+ while (b > 0) {
18095
+ const t = a % b;
18096
+ a = b;
18097
+ b = t;
18098
+ }
18099
+ return a;
18100
+ }
18101
+ /** Check if an item of size (rowSpan × colSpan) can be placed at (row, col). */
18102
+ function canPlace(occupied, row, col, rowSpan, colSpan) {
18103
+ for (let r = row; r < row + rowSpan; r++) {
18104
+ for (let c = col; c < col + colSpan; c++) {
18105
+ if (occupied[r][c])
18106
+ return false;
18107
+ }
18108
+ }
18109
+ return true;
18110
+ }
18111
+ /** Mark cells as occupied for an item placed at (row, col). */
18112
+ function markOccupied(occupied, row, col, rowSpan, colSpan) {
18113
+ for (let r = row; r < row + rowSpan; r++) {
18114
+ for (let c = col; c < col + colSpan; c++)
18115
+ occupied[r][c] = true;
18116
+ }
18117
+ }
18118
+
18119
+ /*
18120
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
18121
+ *
18122
+ * Licensed under EUPL, Version 1.2 (the "License");
18123
+ * you may not use this file except in compliance with the License.
18124
+ * You may obtain a copy of the License at
18125
+ *
18126
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
18127
+ *
18128
+ * Unless required by applicable law or agreed to in writing, software
18129
+ * distributed under the License is distributed on an "AS IS" basis,
18130
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18131
+ * See the License for the specific language governing permissions and
18132
+ * limitations under the License.
18133
+ */
18134
+ /**
18135
+ * Selectable widget layout algorithm. Persisted (optionally) per case widget
18136
+ * tab, IKO tab and dashboard; an absent value falls back to MUURI_GAP_FREE.
18137
+ *
18138
+ * - MUURI_GAP_FREE ("Default (less gaps)"): Muuri's built-in masonry with gap
18139
+ * filling — the behaviour from before the layout-algorithm work. Used when
18140
+ * nothing is configured.
18141
+ * - MUURI ("Default"): Muuri's plain masonry, without gap filling.
18142
+ * - BEAUTIFUL ("Beautiful (gap-free)"): the custom search-based gap-free packing.
18143
+ */
18144
+ var WidgetLayout;
18145
+ (function (WidgetLayout) {
18146
+ WidgetLayout["MUURI_GAP_FREE"] = "MUURI_GAP_FREE";
18147
+ WidgetLayout["MUURI"] = "MUURI";
18148
+ WidgetLayout["BEAUTIFUL"] = "BEAUTIFUL";
18149
+ })(WidgetLayout || (WidgetLayout = {}));
18150
+ /** Row-unit height used by the default and plain Muuri algorithms. */
18151
+ const WIDGET_ROW_HEIGHT_DEFAULT = 200;
18152
+ /** Finer row-unit height the beautiful gap-free algorithm packs against. */
18153
+ const WIDGET_ROW_HEIGHT_GAP_FREE = 92;
18154
+ /**
18155
+ * Resolve a (possibly absent) layout choice to a concrete Muuri `layout` option
18156
+ * and the row-unit height widgets should snap to. Unknown/absent falls back to
18157
+ * MUURI_GAP_FREE (gap-filling Muuri — the pre-existing behaviour).
18158
+ */
18159
+ function resolveWidgetLayout(layout) {
18160
+ switch (layout) {
18161
+ case WidgetLayout.BEAUTIFUL:
18162
+ return { muuriLayout: muuriGapFreeLayout, rowHeightUnit: WIDGET_ROW_HEIGHT_GAP_FREE };
18163
+ case WidgetLayout.MUURI:
18164
+ return { muuriLayout: { fillGaps: false }, rowHeightUnit: WIDGET_ROW_HEIGHT_DEFAULT };
18165
+ case WidgetLayout.MUURI_GAP_FREE:
18166
+ default:
18167
+ return { muuriLayout: { fillGaps: true }, rowHeightUnit: WIDGET_ROW_HEIGHT_DEFAULT };
18168
+ }
18169
+ }
18170
+ /** Selectable layout values in display order, for admin dropdowns. */
18171
+ const WIDGET_LAYOUT_VALUES = [
18172
+ WidgetLayout.MUURI_GAP_FREE,
18173
+ WidgetLayout.MUURI,
18174
+ WidgetLayout.BEAUTIFUL,
18175
+ ];
18176
+ /** Translation keys for each layout value (under the shared `widgetLayout` namespace). */
18177
+ const WIDGET_LAYOUT_TRANSLATION_KEYS = {
18178
+ [WidgetLayout.MUURI_GAP_FREE]: 'widgetLayout.muuriGapFree',
18179
+ [WidgetLayout.MUURI]: 'widgetLayout.muuri',
18180
+ [WidgetLayout.BEAUTIFUL]: 'widgetLayout.beautiful',
18181
+ };
18182
+
17759
18183
  /*
17760
18184
  * Copyright 2015-2025 Ritense BV, the Netherlands.
17761
18185
  *
@@ -17828,9 +18252,7 @@ class MuuriDirective {
17828
18252
  return;
17829
18253
  }
17830
18254
  this._muuriSubject$.next(new Muuri(nativeElement, {
17831
- layout: {
17832
- fillGaps: true,
17833
- },
18255
+ layout: resolveWidgetLayout(this.widgetLayout).muuriLayout,
17834
18256
  layoutOnResize: false,
17835
18257
  }));
17836
18258
  }
@@ -17873,7 +18295,7 @@ class MuuriDirective {
17873
18295
  this._muuri.refreshItems();
17874
18296
  this._muuri.layout(true);
17875
18297
  }
17876
- }, 50);
18298
+ }, 200);
17877
18299
  }
17878
18300
  observeContainerWidthChanges() {
17879
18301
  const nativeElement = this.elementRef.nativeElement;
@@ -17940,7 +18362,7 @@ class MuuriDirective {
17940
18362
  this.renderer.setStyle(el, 'margin', '-8px');
17941
18363
  }
17942
18364
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MuuriDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); }
17943
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.20", type: MuuriDirective, isStandalone: true, selector: "[muuri]", inputs: { columnMinWidth: "columnMinWidth" }, ngImport: i0 }); }
18365
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.20", type: MuuriDirective, isStandalone: true, selector: "[muuri]", inputs: { columnMinWidth: "columnMinWidth", widgetLayout: "widgetLayout" }, ngImport: i0 }); }
17944
18366
  }
17945
18367
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: MuuriDirective, decorators: [{
17946
18368
  type: Directive,
@@ -17950,6 +18372,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
17950
18372
  }]
17951
18373
  }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }], propDecorators: { columnMinWidth: [{
17952
18374
  type: Input
18375
+ }], widgetLayout: [{
18376
+ type: Input
17953
18377
  }] } });
17954
18378
 
17955
18379
  /*
@@ -18021,6 +18445,35 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
18021
18445
  }]
18022
18446
  }] });
18023
18447
 
18448
+ /*
18449
+ * Copyright 2015-2026 Ritense BV, the Netherlands.
18450
+ *
18451
+ * Licensed under EUPL, Version 1.2 (the "License");
18452
+ * you may not use this file except in compliance with the License.
18453
+ * You may obtain a copy of the License at
18454
+ *
18455
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
18456
+ *
18457
+ * Unless required by applicable law or agreed to in writing, software
18458
+ * distributed under the License is distributed on an "AS IS" basis,
18459
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18460
+ * See the License for the specific language governing permissions and
18461
+ * limitations under the License.
18462
+ */
18463
+ /**
18464
+ * Informational Carbon notification explaining the widget layout algorithms.
18465
+ * Rendered next to every layout-algorithm selector (case widget tab, IKO tab
18466
+ * and dashboard) so the trade-offs are explained consistently.
18467
+ */
18468
+ class WidgetLayoutInfoComponent {
18469
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: WidgetLayoutInfoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18470
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: WidgetLayoutInfoComponent, isStandalone: true, selector: "valtimo-widget-layout-info", ngImport: i0, template: "<cds-inline-notification\n [notificationObj]=\"{\n type: 'info',\n title: 'widgetLayout.infoTitle' | translate,\n message: 'widgetLayout.info' | translate,\n showClose: false,\n lowContrast: true\n }\"\n></cds-inline-notification>\n", dependencies: [{ kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i1.TranslatePipe, name: "translate" }, { kind: "ngmodule", type: NotificationModule }, { kind: "component", type: i2$3.Notification, selector: "cds-notification, cds-inline-notification, ibm-notification, ibm-inline-notification", inputs: ["notificationObj"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
18471
+ }
18472
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: WidgetLayoutInfoComponent, decorators: [{
18473
+ type: Component,
18474
+ args: [{ selector: 'valtimo-widget-layout-info', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [TranslateModule, NotificationModule], template: "<cds-inline-notification\n [notificationObj]=\"{\n type: 'info',\n title: 'widgetLayout.infoTitle' | translate,\n message: 'widgetLayout.info' | translate,\n showClose: false,\n lowContrast: true\n }\"\n></cds-inline-notification>\n" }]
18475
+ }] });
18476
+
18024
18477
  /*
18025
18478
  * Copyright 2015-2025 Ritense BV, the Netherlands.
18026
18479
  *
@@ -18675,5 +19128,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
18675
19128
  * Generated bundle index. Do not edit.
18676
19129
  */
18677
19130
 
18678
- export { ARBITRARY_AMOUNT_VALUE_TEST_IDS, AUTO_KEY_INPUT_TEST_IDS, AdminSettingsService, Alert, AlertComponent, AlertModule, AlertService, AlertType, AlertsOptionsImpl, AssignmentComponent, AutoKeyInputComponent, BOOLEAN_CONVERTER_VALUES, BpmnJsDiagramComponent, BpmnJsDiagramModule, BreadcrumbNavigationComponent, BreadcrumbNavigationModule, BreadcrumbService, CARBON_CONSTANTS, CARBON_THEME, CASES_WITHOUT_STATUS_KEY, COLOR_PICKER_TEST_IDS, CONFIRMATION_MODAL_TEST_IDS, CamundaFormComponent, CamundaFormModule, CarbonListComponent, CarbonListFilterPipe, CarbonListModule, CarbonMultiInputComponent, CarbonMultiInputModule, CarbonNoResultsComponent, CaseCountPipe, CaseTagsSelectorComponent, CdsThemeService, ChoiceFieldService, ColorPickerComponent, ComponentsPipesModule, ConfirmationModalComponent, ConfirmationModalModule, ContextMenuDirective, CtrlClickDirective, CurrentCarbonTheme, DEFAULT_LIST_TRANSLATIONS, DEFAULT_PAGINATION, DEFAULT_PAGINATOR_CONFIG, DataListComponent, DataListModule, DatePickerComponent, DatePickerModule, DateTimePickerComponent, DigitOnlyDirective, DropzoneComponent, DropzoneModule, EditorComponent, EditorModule, EllipsisPipe, ExpansionPanelComponent, ExpansionPanelModule, FieldAutoFocusDirective, FieldAutoFocusModule, FileSizeModule, FileSizePipe, FilterSidebarComponent, FilterSidebarModule, FitPageDirective, FormComponent, FormIoCurrencyComponent, FormIoCurrentUserComponent, FormIoDomService, FormIoIbanComponent, FormIoModule, FormIoStateService, FormIoTagsService, FormIoUploaderComponent, FormModule, FormioBuilderComponent, FormioComponent, FormioDummyComponent, FormioOptionsImpl, FormioValueResolverSelectorComponent, InputComponent, InputLabelComponent, InputLabelModule, InputModule, IsArrayPipe, JSON_EDITOR_TEST_IDS, JsonEditorComponent, KEY_DROPDOWN_VALUE_TEST_IDS, KEY_VALUE_PATH_SELECTOR_TEST_IDS, KEY_VALUE_TEST_IDS, KeyGeneratorService, LeftSidebarComponent, LeftSidebarModule, ListColumnViewComponent, MdiIconSelectorComponent, MdiIconViewerComponent, MenuItemTextComponent, MenuItemTranslationPipe, MenuModule, MenuRoutingModule, MenuService, ModalComponent, ModalModule, ModalService, MonacoTheme, MoveRowDirection, MultiInputFormComponent, MultiInputFormModule, MultiselectDropdownComponent, MultiselectDropdownModule, MuuriDirective, MuuriDirectiveModule, MuuriItemComponent, ObserveSizeDirective, OverflowMenuComponent, OverflowMenuModule, OverflowMenuOptionComponent, OverflowMenuTriggerComponent, POPULAR_MDI_ICONS, PageHeaderComponent, PageHeaderModule, PageHeaderService, PageSubtitleService, PageTitleComponent, PageTitleModule, PageTitleService, ParagraphComponent, ParagraphModule, PendingChangesComponent, PendingChangesService, ProgressBarComponent, ProgressBarModule, PromptComponent, PromptModule, PromptService, QUICK_SEARCH_SERVICE, QuickSearchComponent, QuickSearchStateService, RIGHT_SIDEBAR_TEST_IDS, RadioComponent, RadioModule, ReadOnlyDirective, RemoveClassnamesDirective, RenderInBodyComponent, RenderInPageHeaderDirective, RenderPageHeaderDirective, RightSidebarComponent, RightSidebarModule, SINGLE_VALUE_TEST_IDS, STEPPER_FOOTER_STEP_TEST_IDS, SchemaEditorComponent, SearchFieldsComponent, SearchFieldsModule, SelectComponent, SelectModule, SelectableCarbonTheme, ShellService, SpinnerComponent, SpinnerModule, StatusSelectorComponent, StepperContainerComponent, StepperContentComponent, StepperFooterComponent, StepperFooterStepComponent, StepperHeaderComponent, StepperModule, StepperService, StepperStepComponent, TAG_ELLIPSIS_LIMIT, TableComponent, TableModule, TimelineComponent, TimelineItemImpl, TimelineModule, TooltipComponent, TooltipDirective, TooltipIconComponent, TooltipIconModule, TooltipModule, TopbarComponent, TopbarModule, UploaderComponent, UploaderDragDropDirective, UploaderModule, UserInterfaceService, VALUE_PATH_SELECTOR_DROPDOWN_VALUE_TEST_IDS, VALUE_PATH_SELECTOR_TEST_IDS, VALUE_PATH_SELECTOR_VALUE_TEST_IDS, VModalComponent, VModalModule, ValtimoCdsModalDirective, ValtimoModalService, ValuePathSelectorComponent, ValuePathSelectorInputMode, ValuePathSelectorPrefix, ValuePathSelectorService, ValuePathType, ViewContentService, ViewType, WebcamComponent, WebcamModule, WidgetComponent, WidgetModule, applyDataGridPatch, collectObjectLevels, createCustomFormioComponent, customUploaderType, enableCustomFormioComponents, menuInitializer, pendingChangesGuard, registerCustomFormioComponent, registerCustomTag, registerFormioCurrencyComponent, registerFormioCurrentUserComponent, registerFormioFileSelectorComponent, registerFormioIbanComponent, registerFormioUploadComponent, registerFormioValueResolverSelectorComponent, runAfterCarbonModalClosed, setRequiredOnSchema };
19131
+ export { ARBITRARY_AMOUNT_VALUE_TEST_IDS, AUTO_KEY_INPUT_TEST_IDS, AdminSettingsService, Alert, AlertComponent, AlertModule, AlertService, AlertType, AlertsOptionsImpl, AssignmentComponent, AutoKeyInputComponent, BOOLEAN_CONVERTER_VALUES, BpmnJsDiagramComponent, BpmnJsDiagramModule, BreadcrumbNavigationComponent, BreadcrumbNavigationModule, BreadcrumbService, CARBON_CONSTANTS, CARBON_THEME, CASES_WITHOUT_STATUS_KEY, COLOR_PICKER_TEST_IDS, CONFIRMATION_MODAL_TEST_IDS, CamundaFormComponent, CamundaFormModule, CarbonListComponent, CarbonListFilterPipe, CarbonListModule, CarbonMultiInputComponent, CarbonMultiInputModule, CarbonNoResultsComponent, CaseCountPipe, CaseTagsSelectorComponent, CdsThemeService, ChoiceFieldService, ColorPickerComponent, ComponentsPipesModule, ConfirmationModalComponent, ConfirmationModalModule, ContextMenuDirective, CtrlClickDirective, CurrentCarbonTheme, DEFAULT_LIST_TRANSLATIONS, DEFAULT_PAGINATION, DEFAULT_PAGINATOR_CONFIG, DataListComponent, DataListModule, DatePickerComponent, DatePickerModule, DateTimePickerComponent, DigitOnlyDirective, DropzoneComponent, DropzoneModule, EditorComponent, EditorModule, EllipsisPipe, ExpansionPanelComponent, ExpansionPanelModule, FieldAutoFocusDirective, FieldAutoFocusModule, FileSizeModule, FileSizePipe, FilterSidebarComponent, FilterSidebarModule, FitPageDirective, FormComponent, FormIoCurrencyComponent, FormIoCurrentUserComponent, FormIoDomService, FormIoIbanComponent, FormIoModule, FormIoStateService, FormIoTagsService, FormIoUploaderComponent, FormModule, FormioBuilderComponent, FormioComponent, FormioDummyComponent, FormioOptionsImpl, FormioValueResolverSelectorComponent, InputComponent, InputLabelComponent, InputLabelModule, InputModule, IsArrayPipe, JSON_EDITOR_TEST_IDS, JsonEditorComponent, KEY_DROPDOWN_VALUE_TEST_IDS, KEY_VALUE_PATH_SELECTOR_TEST_IDS, KEY_VALUE_TEST_IDS, KeyGeneratorService, LeftSidebarComponent, LeftSidebarModule, ListColumnViewComponent, MdiIconSelectorComponent, MdiIconViewerComponent, MenuItemTextComponent, MenuItemTranslationPipe, MenuModule, MenuRoutingModule, MenuService, ModalComponent, ModalModule, ModalService, MonacoTheme, MoveRowDirection, MultiInputFormComponent, MultiInputFormModule, MultiselectDropdownComponent, MultiselectDropdownModule, MuuriDirective, MuuriDirectiveModule, MuuriItemComponent, ObserveSizeDirective, OverflowMenuComponent, OverflowMenuModule, OverflowMenuOptionComponent, OverflowMenuTriggerComponent, POPULAR_MDI_ICONS, PageHeaderComponent, PageHeaderModule, PageHeaderService, PageSubtitleService, PageTitleComponent, PageTitleModule, PageTitleService, ParagraphComponent, ParagraphModule, PendingChangesComponent, PendingChangesService, ProgressBarComponent, ProgressBarModule, PromptComponent, PromptModule, PromptService, QUICK_SEARCH_SERVICE, QuickSearchComponent, QuickSearchStateService, RIGHT_SIDEBAR_TEST_IDS, RadioComponent, RadioModule, ReadOnlyDirective, RemoveClassnamesDirective, RenderInBodyComponent, RenderInPageHeaderDirective, RenderPageHeaderDirective, RightSidebarComponent, RightSidebarModule, SINGLE_VALUE_TEST_IDS, STEPPER_FOOTER_STEP_TEST_IDS, SchemaEditorComponent, SearchFieldsComponent, SearchFieldsModule, SelectComponent, SelectModule, SelectableCarbonTheme, ShellService, SpinnerComponent, SpinnerModule, StatusSelectorComponent, StepperContainerComponent, StepperContentComponent, StepperFooterComponent, StepperFooterStepComponent, StepperHeaderComponent, StepperModule, StepperService, StepperStepComponent, TAG_ELLIPSIS_LIMIT, TableComponent, TableModule, TimelineComponent, TimelineItemImpl, TimelineModule, TooltipComponent, TooltipDirective, TooltipIconComponent, TooltipIconModule, TooltipModule, TopbarComponent, TopbarModule, UploaderComponent, UploaderDragDropDirective, UploaderModule, UserInterfaceService, VALUE_PATH_SELECTOR_DROPDOWN_VALUE_TEST_IDS, VALUE_PATH_SELECTOR_TEST_IDS, VALUE_PATH_SELECTOR_VALUE_TEST_IDS, VModalComponent, VModalModule, ValtimoCdsModalDirective, ValtimoModalService, ValuePathSelectorComponent, ValuePathSelectorInputMode, ValuePathSelectorPrefix, ValuePathSelectorService, ValuePathType, ViewContentService, ViewType, WIDGET_LAYOUT_TRANSLATION_KEYS, WIDGET_LAYOUT_VALUES, WIDGET_ROW_HEIGHT_DEFAULT, WIDGET_ROW_HEIGHT_GAP_FREE, WebcamComponent, WebcamModule, WidgetComponent, WidgetLayout, WidgetLayoutInfoComponent, WidgetModule, applyDataGridPatch, collectObjectLevels, createCustomFormioComponent, customUploaderType, enableCustomFormioComponents, menuInitializer, muuriGapFreeLayout, pendingChangesGuard, registerCustomFormioComponent, registerCustomTag, registerFormioCurrencyComponent, registerFormioCurrentUserComponent, registerFormioFileSelectorComponent, registerFormioIbanComponent, registerFormioUploadComponent, registerFormioValueResolverSelectorComponent, resolveWidgetLayout, runAfterCarbonModalClosed, setRequiredOnSchema };
18679
19132
  //# sourceMappingURL=valtimo-components.mjs.map