@massimo-cassandro/minimo 1.30.0 → 1.31.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.
@@ -1,5 +1,5 @@
1
1
  import { parseStdAxisOpts } from './helpers/parse-std-axis-opts.js';
2
- import { parseContainer } from './helpers/chart-utils.js';
2
+ import { parseContainer, waitForContainerSize } from './helpers/chart-utils.js';
3
3
  import { cartesianAxis } from './helpers/cartesian-axis.js';
4
4
  import { legenda } from './helpers/legenda.js';
5
5
 
@@ -185,13 +185,15 @@ export async function bars(params) {
185
185
  }
186
186
 
187
187
  // container
188
- [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
188
+ let containerElement;
189
+ [containerElement, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
189
190
 
190
-
191
- if(!params.width || !params.height) {
191
+ if(!containerElement && (!params.width || !params.height)) {
192
192
  throw `width e/o height mancanti: width: ${params.width}, height: ${params.height}`;
193
193
  }
194
194
 
195
+ ({ width: params.width, height: params.height } = await waitForContainerSize(containerElement, { width: params.width, height: params.height }, params.debug));
196
+
195
197
  // =>> legenda
196
198
  // costruzione delle opzioni per la legenda
197
199
  const legenda_params = {
@@ -1,7 +1,7 @@
1
1
  // mostra un triangolo rettangolo disposto orizzontalmente che mostra
2
2
  // la porzione di raggiungimento di un obiettivo
3
3
 
4
- import { parseContainer, getElementFromContainer } from './helpers/chart-utils.js';
4
+ import { parseContainer, getElementFromContainer, waitForContainerSize } from './helpers/chart-utils.js';
5
5
  import { createTextEl } from './helpers/create-text-element.js';
6
6
 
7
7
  export async function goal({
@@ -158,11 +158,12 @@ export async function goal({
158
158
  // calcolo width
159
159
  [, width] = parseContainer({ container: container, width: width, height: height });
160
160
 
161
-
162
- if(!width || !height) {
161
+ if(!container && (!width || !height)) {
163
162
  throw `width e/o height mancanti: width: ${width}, height: ${height} (chartHeight: ${chartHeight})`;
164
163
  }
165
164
 
165
+ ({ width, height } = await waitForContainerSize(container, { width, height }, debug));
166
+
166
167
  // larghezza iniziale grafico
167
168
  // al netto del padding orizzontale pari a 1/2 cursore, in modo che sia visibile interamente
168
169
  // quando posizionato agli estremi (anche in presenta di etichetta laterale, nel caso quest'ultima sia più alta del grafico)
@@ -2,7 +2,7 @@
2
2
  // categorie sull'asse verticale (a sinistra), valori sull'asse orizzontale (in basso)
3
3
 
4
4
  import { parseStdAxisOpts } from './helpers/parse-std-axis-opts.js';
5
- import { parseContainer } from './helpers/chart-utils.js';
5
+ import { parseContainer, waitForContainerSize } from './helpers/chart-utils.js';
6
6
  import { cartesianAxis } from './helpers/cartesian-axis.js';
7
7
  import { legenda } from './helpers/legenda.js';
8
8
 
@@ -186,13 +186,15 @@ export async function hbars(params) {
186
186
  }
187
187
 
188
188
  // container
189
- [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
189
+ let containerElement;
190
+ [containerElement, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
190
191
 
191
-
192
- if(!params.width || !params.height) {
192
+ if(!containerElement && (!params.width || !params.height)) {
193
193
  throw `width e/o height mancanti: width: ${params.width}, height: ${params.height}`;
194
194
  }
195
195
 
196
+ ({ width: params.width, height: params.height } = await waitForContainerSize(containerElement, { width: params.width, height: params.height }, params.debug));
197
+
196
198
  // =>> legenda
197
199
  // costruzione delle opzioni per la legenda
198
200
  const legenda_params = {
@@ -155,3 +155,50 @@ export function parseContainer({ container = null, width = null, height = null }
155
155
 
156
156
  return [containerElement, width, height];
157
157
  }
158
+
159
+ /**
160
+ * Attende che il container abbia dimensioni valide, utile quando è temporaneamente
161
+ * nascosto (es. `display: none` dentro un tab o un pannello non ancora attivo) al
162
+ * momento della creazione del grafico. Se `width`/`height` sono già entrambe valide
163
+ * risolve immediatamente, altrimenti osserva il container con un `ResizeObserver` ed
164
+ * attende che torni visibile. Non ha timeout: se il container non torna mai visibile
165
+ * la promise resta in sospeso.
166
+ * @param {Element|null} containerElement - L'elemento container (può essere null).
167
+ * @param {Object} size - Dimensioni già note.
168
+ * @param {number|null} size.width - Larghezza già nota (se valida, non si attende il container).
169
+ * @param {number|null} size.height - Altezza già nota (se valida, non si attende il container).
170
+ * @param {boolean} [debug=false] - Se true, logga in console quando l'attesa ha inizio. (default: false)
171
+ * @returns {Promise<{width: number, height: number}>} Le dimensioni effettive una volta disponibili.
172
+ */
173
+ export function waitForContainerSize(containerElement, { width = null, height = null } = {}, debug = false) {
174
+ if (width && height) {
175
+ return Promise.resolve({ width, height });
176
+ }
177
+
178
+ if (!containerElement) {
179
+ return Promise.resolve({ width, height });
180
+ }
181
+
182
+ return new Promise(resolve => {
183
+
184
+ if (debug) {
185
+ // eslint-disable-next-line no-console
186
+ console.warn('MinimoCharts → in attesa che il container ottenga dimensioni valide (es. tab/pannello nascosto)', containerElement);
187
+ }
188
+
189
+ const observer = new ResizeObserver(entries => {
190
+ for (const entry of entries) {
191
+ const observedWidth = width || entry.contentRect.width;
192
+ const observedHeight = height || entry.contentRect.height;
193
+
194
+ if (observedWidth && observedHeight) {
195
+ observer.disconnect();
196
+ resolve({ width: observedWidth, height: observedHeight });
197
+ return;
198
+ }
199
+ }
200
+ });
201
+
202
+ observer.observe(containerElement);
203
+ });
204
+ }
@@ -1,5 +1,5 @@
1
1
  import { parseStdAxisOpts } from './helpers/parse-std-axis-opts.js';
2
- import { parseContainer } from './helpers/chart-utils.js';
2
+ import { parseContainer, waitForContainerSize } from './helpers/chart-utils.js';
3
3
  import { cartesianAxis } from './helpers/cartesian-axis.js';
4
4
  import { legenda } from './helpers/legenda.js';
5
5
 
@@ -196,12 +196,15 @@ export async function lines(params) {
196
196
 
197
197
 
198
198
  // container
199
- [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
199
+ let containerElement;
200
+ [containerElement, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
200
201
 
201
- if(!params.width || !params.height) {
202
+ if(!containerElement && (!params.width || !params.height)) {
202
203
  throw `width e/o height mancanti: width: ${params.width}, height: ${params.height}`;
203
204
  }
204
205
 
206
+ ({ width: params.width, height: params.height } = await waitForContainerSize(containerElement, { width: params.width, height: params.height }, params.debug));
207
+
205
208
  // =>> ricalcolo data_lines_attrs (utilizzato anche per le linee della legenda)
206
209
  const default_line_width = params.data_lines_attrs['stroke-width']?? 1;
207
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.30.0",
3
+ "version": "1.31.1",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -27,6 +27,54 @@ function closeDialog(el) {
27
27
  }, { once: true });
28
28
  }
29
29
 
30
+ /**
31
+ * Wraps a single domBuilder object (not an array, not a DOM Node) into an array,
32
+ * so `content`/`headerContent`/`footerContent` can be handled uniformly with domBuilder arrays.
33
+ * @param {*} value
34
+ * @returns {*}
35
+ */
36
+ function normalizeContentParam(value) {
37
+ if(value != null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Node)) {
38
+ return [value];
39
+ }
40
+ return value;
41
+ }
42
+
43
+ /**
44
+ * @param {*} value - a non-null `content`/`headerContent`/`footerContent` value
45
+ * @returns {'domBuilder' | 'node' | 'function' | 'html'}
46
+ */
47
+ function getContentKind(value) {
48
+ if(Array.isArray(value)) {
49
+ return 'domBuilder';
50
+ }
51
+ if(value instanceof Node) {
52
+ return 'node';
53
+ }
54
+ if(typeof value === 'function') {
55
+ return 'function';
56
+ }
57
+ return 'html';
58
+ }
59
+
60
+ /**
61
+ * Resolves the `content`/`children` domBuilder props for a `headerContent`/`footerContent` value.
62
+ * The `function` kind is not handled here: it is invoked separately, from the item's `callback`,
63
+ * once the container element exists.
64
+ * @param {*} value
65
+ * @returns {{content: string | Node | null, children: DomBuilderItem[] | undefined}}
66
+ */
67
+ function getDomBuilderContentProps(value) {
68
+ if(value == null) {
69
+ return { content: null, children: undefined };
70
+ }
71
+ const kind = getContentKind(value);
72
+ return {
73
+ content: (kind === 'html' || kind === 'node') ? value : null,
74
+ children: kind === 'domBuilder' ? value : undefined
75
+ };
76
+ }
77
+
30
78
  /**
31
79
  * modalPopup
32
80
  * Opens a popup dialog with the given content.
@@ -40,7 +88,7 @@ function closeDialog(el) {
40
88
  *
41
89
  * @param {Object} params
42
90
  * @param {string | null} [params.dialogExtraClassName=null] - Extra class added to the dialog element. (default: null)
43
- * @param {string | DomBuilderItem | DomBuilderItem[] | null} [params.content=null] - Plain text, HTML, a domBuilder object, or a domBuilder array. (default: null)
91
+ * @param {string | DomBuilderItem | DomBuilderItem[] | Node | ((container: HTMLElement) => void) | null} [params.content=null] - Plain text, HTML, a domBuilder object, a domBuilder array, a DOM node (appended as-is), or a function invoked with the content container element to populate it directly (`content(dialogContentEl)`). (default: null)
44
92
  * @param {string | null} [params.contentExtraClassName=null] - Extra class added to the content wrapper. (default: null)
45
93
  * @param {HTMLElement | null} [params.triggerElement=null] - Optional element that triggered the popup; when set, `aria-haspopup`, `aria-controls` and `aria-expanded` are managed on it. (default: null)
46
94
  * @param {boolean} [params.addFocus=true] - if true, dialog receives focus after opening (default: true)
@@ -58,8 +106,8 @@ function closeDialog(el) {
58
106
  * @param {((el: HTMLDialogElement) => void) | null} [params.closeCallback=null] - Called with the dialog element just before it is removed, receives the dialog element (`closeCallback(dialogEl)`). NB: `dialogEl` is removed immediately after closing. (default: null)
59
107
  * @param {((data: *, el: Element) => void) | null} [params.ajaxCallback=null] - Called with the Ajax response and the content element. (default: null)
60
108
  * @param {boolean} [params.addScrollbarPadding=false] - Adds right padding to compensate for the scrollbar. (default: false)
61
- * @param {string | DomBuilderItem | DomBuilderItem[] | null} [params.headerContent=null] - Header content: plain text, HTML, a domBuilder object, or a domBuilder array. (default: null)
62
- * @param {string | DomBuilderItem | DomBuilderItem[] | null} [params.footerContent=null] - Footer content: plain text, HTML, a domBuilder object, or a domBuilder array. (default: null)
109
+ * @param {string | DomBuilderItem | DomBuilderItem[] | Node | ((container: HTMLElement) => void) | null} [params.headerContent=null] - Header content: plain text, HTML, a domBuilder object, a domBuilder array, a DOM node, or a function invoked with the header container element to populate it directly. (default: null)
110
+ * @param {string | DomBuilderItem | DomBuilderItem[] | Node | ((container: HTMLElement) => void) | null} [params.footerContent=null] - Footer content: same accepted types as `headerContent`, invoked with the footer container element. (default: null)
63
111
  * @returns {HTMLDialogElement} The dialog element.
64
112
  */
65
113
 
@@ -84,7 +132,7 @@ export function modalPopup({
84
132
  /** iframe url */
85
133
  iframeUrl = null,
86
134
 
87
- /** content: plain text, html, domBuilder object or domBuilder array */
135
+ /** content: plain text, html, domBuilder object/array, DOM node, or function(container) */
88
136
  content = null,
89
137
 
90
138
  /** Ajax url and callback.
@@ -98,13 +146,13 @@ export function modalPopup({
98
146
  closeCallback = null,
99
147
  addScrollbarPadding = false, // adds extra right padding to compensate for the scrollbar
100
148
 
101
- /** header content: plain text, HTML, domBuilder object or domBuilder array */
149
+ /** header content: plain text, HTML, domBuilder object/array, DOM node, or function(container) */
102
150
  headerContent = null,
103
151
 
104
152
  /** extra classname added to header */
105
153
  headerExtraClassName = null,
106
154
 
107
- /** footer content: plain text, HTML, domBuilder object or domBuilder array */
155
+ /** footer content: plain text, HTML, domBuilder object/array, DOM node, or function(container) */
108
156
  footerContent = null,
109
157
 
110
158
  /** extra classname added to footer */
@@ -121,22 +169,13 @@ export function modalPopup({
121
169
  }
122
170
 
123
171
  // allow a single domBuilder object (in addition to a domBuilder array) by wrapping it in an array
124
- if(content != null && typeof content === 'object' && !Array.isArray(content)) {
125
- content = [content];
126
- }
127
- if(headerContent != null && typeof headerContent === 'object' && !Array.isArray(headerContent)) {
128
- headerContent = [headerContent];
129
- }
130
- if(footerContent != null && typeof footerContent === 'object' && !Array.isArray(footerContent)) {
131
- footerContent = [footerContent];
132
- }
172
+ content = normalizeContentParam(content);
173
+ headerContent = normalizeContentParam(headerContent);
174
+ footerContent = normalizeContentParam(footerContent);
133
175
 
134
176
  let mode;
135
- if(content != null && Array.isArray(content)) {
136
- mode = 'domBuilder';
137
-
138
- } else if (content != null) {
139
- mode = 'html';
177
+ if(content != null) {
178
+ mode = getContentKind(content);
140
179
 
141
180
  } else if (ajaxUrl != null && ajaxCallback != null) {
142
181
  mode = 'ajax';
@@ -210,13 +249,17 @@ export function modalPopup({
210
249
  id: headerId,
211
250
  className: classnames(styles.header, headerExtraClassName),
212
251
  condition: headerContent != null,
213
- content: Array.isArray(headerContent) ? null : headerContent,
214
- children: Array.isArray(headerContent) ? headerContent : undefined
252
+ ...getDomBuilderContentProps(headerContent),
253
+ callback: el => {
254
+ if(headerContent != null && getContentKind(headerContent) === 'function') {
255
+ /** @type {(container: HTMLElement) => void} */ (headerContent)(/** @type {HTMLElement} */ (el));
256
+ }
257
+ }
215
258
  },
216
259
  {
217
260
  className: classnames(styles.content, contentExtraClassName),
218
- content: mode === 'html'
219
- ? content
261
+ content: (mode === 'html' || mode === 'node')
262
+ ? /** @type {string | Node} */ (content)
220
263
  : mode === 'ajax'
221
264
  ? spinner()
222
265
  : null,
@@ -233,7 +276,11 @@ export function modalPopup({
233
276
  }
234
277
  ],
235
278
  callback: el => {
236
- dialogContentEl = el;
279
+ dialogContentEl = /** @type {HTMLElement} */ (el);
280
+
281
+ if(mode === 'function') {
282
+ /** @type {(container: HTMLElement) => void} */ (content)(dialogContentEl);
283
+ }
237
284
 
238
285
  if(mode === 'ajax') {
239
286
  try {
@@ -241,7 +288,7 @@ export function modalPopup({
241
288
  (async () => {
242
289
  const response = await fetch(safeAjaxUrl),
243
290
  data = await response.json();
244
- el.innerHTML = '';
291
+ dialogContentEl.innerHTML = '';
245
292
  ajaxCallback?.(data, dialogContentEl);
246
293
  })();
247
294
 
@@ -258,8 +305,12 @@ export function modalPopup({
258
305
  {
259
306
  className: classnames(styles.footer, footerExtraClassName),
260
307
  condition: footerContent != null,
261
- content: Array.isArray(footerContent) ? null : footerContent,
262
- children: Array.isArray(footerContent) ? footerContent : undefined
308
+ ...getDomBuilderContentProps(footerContent),
309
+ callback: el => {
310
+ if(footerContent != null && getContentKind(footerContent) === 'function') {
311
+ /** @type {(container: HTMLElement) => void} */ (footerContent)(/** @type {HTMLElement} */ (el));
312
+ }
313
+ }
263
314
  },
264
315
  ]
265
316
  }