@rettangoli/ui 1.7.4 → 1.7.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/ui",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "A UI component library for building web interfaces.",
5
5
  "main": "dist/rettangoli-esm.min.js",
6
6
  "type": "module",
@@ -0,0 +1,74 @@
1
+ export const calculatePopoverPosition = ({
2
+ x,
3
+ y,
4
+ width,
5
+ height,
6
+ place,
7
+ viewportWidth,
8
+ viewportHeight,
9
+ offset = 8,
10
+ padding = 8,
11
+ }) => {
12
+ let left = x;
13
+ let top = y;
14
+
15
+ switch (place) {
16
+ case "t":
17
+ left = x - width / 2;
18
+ top = y - height - offset;
19
+ break;
20
+ case "ts":
21
+ left = x;
22
+ top = y - height - offset;
23
+ break;
24
+ case "te":
25
+ left = x - width;
26
+ top = y - height - offset;
27
+ break;
28
+ case "r":
29
+ left = x + offset;
30
+ top = y - height / 2;
31
+ break;
32
+ case "rs":
33
+ left = x + offset;
34
+ top = y;
35
+ break;
36
+ case "re":
37
+ left = x + offset;
38
+ top = y - height;
39
+ break;
40
+ case "b":
41
+ left = x - width / 2;
42
+ top = y + offset;
43
+ break;
44
+ case "bs":
45
+ left = x;
46
+ top = y + offset;
47
+ break;
48
+ case "be":
49
+ left = x - width;
50
+ top = y + offset;
51
+ break;
52
+ case "l":
53
+ left = x - width - offset;
54
+ top = y - height / 2;
55
+ break;
56
+ case "ls":
57
+ left = x - width - offset;
58
+ top = y;
59
+ break;
60
+ case "le":
61
+ left = x - width - offset;
62
+ top = y - height;
63
+ break;
64
+ default:
65
+ left = x;
66
+ top = y + offset;
67
+ break;
68
+ }
69
+
70
+ return {
71
+ left: Math.max(padding, Math.min(left, viewportWidth - width - padding)),
72
+ top: Math.max(padding, Math.min(top, viewportHeight - height - padding)),
73
+ };
74
+ };
@@ -11,6 +11,18 @@ const getItemType = (item = {}) => {
11
11
  return 'item';
12
12
  };
13
13
 
14
+ export const handleOnUpdate = (deps, payload) => {
15
+ const { render, refs } = deps;
16
+ const { oldProps = {}, newProps = {} } = payload;
17
+ const shouldRefreshPopover = oldProps.items !== newProps.items && !!newProps.open;
18
+
19
+ render();
20
+
21
+ if (shouldRefreshPopover) {
22
+ refs?.popover?.refreshContent?.();
23
+ }
24
+ }
25
+
14
26
  export const handleClosePopover = (deps, payload) => {
15
27
  const { dispatchEvent } = deps;
16
28
  dispatchEvent(new CustomEvent('close'));
@@ -69,7 +69,7 @@ template:
69
69
  - $if field.type == "select":
70
70
  - rtgl-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
71
71
  - $if field.type == "tag-select":
72
- - rtgl-tag-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} :selectedValues=${field._selectedValues} :placeholder=${field.placeholder} :addOption=${field.addOption} ?disabled=${field._disabled}: null
72
+ - rtgl-tag-select#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} :selectedValues=${field._selectedValues} :placeholder=${field.placeholder} :addOption=${field.addOption} ?no-add=${field.noAdd} ?disabled=${field._disabled}: null
73
73
  - $if field.type == "segmented-control":
74
74
  - rtgl-segmented-control#field${field._idx} data-field-name=${field.name} w=f :options=${field.options} ?no-clear=${field.noClear} :selectedValue=${field._selectedValue} :placeholder=${field.placeholder} ?disabled=${field._disabled}: null
75
75
  - $if field.type == "color-picker":
@@ -28,8 +28,9 @@ export const handleBeforeMount = (deps) => {
28
28
 
29
29
  export const handleOnUpdate = (deps, payload) => {
30
30
  const { oldProps, newProps } = payload;
31
- const { store, render } = deps;
31
+ const { store, render, refs } = deps;
32
32
  let shouldRender = false;
33
+ let shouldRefreshPopover = false;
33
34
 
34
35
  if (!!newProps?.disabled && !oldProps?.disabled) {
35
36
  store.closeOptionsPopover({});
@@ -41,8 +42,17 @@ export const handleOnUpdate = (deps, payload) => {
41
42
  shouldRender = true;
42
43
  }
43
44
 
45
+ if (oldProps.options !== newProps.options) {
46
+ shouldRender = true;
47
+ shouldRefreshPopover = true;
48
+ }
49
+
44
50
  if (shouldRender) {
45
51
  render();
52
+
53
+ if (shouldRefreshPopover && store.selectState?.().isOpen) {
54
+ refs?.popover?.refreshContent?.();
55
+ }
46
56
  }
47
57
  }
48
58
 
@@ -341,7 +341,7 @@ export const handleSubmitClick = (deps, payload) => {
341
341
 
342
342
  export const handleAddOptionClick = (deps, payload) => {
343
343
  const { props, dispatchEvent } = deps;
344
- if (props.disabled) return;
344
+ if (props.disabled || props.noAdd) return;
345
345
 
346
346
  const event = payload._event;
347
347
  event.stopPropagation();
@@ -44,6 +44,8 @@ propsSchema:
44
44
  properties:
45
45
  label:
46
46
  type: string
47
+ noAdd:
48
+ type: boolean
47
49
  disabled:
48
50
  type: boolean
49
51
  w:
@@ -12,6 +12,7 @@ const blacklistedProps = [
12
12
  "draftSelectedValues",
13
13
  "onChange",
14
14
  "addOption",
15
+ "noAdd",
15
16
  "disabled",
16
17
  ];
17
18
 
@@ -231,7 +232,7 @@ export const selectViewData = ({ state, props }) => {
231
232
  triggerTags,
232
233
  triggerCursor: isDisabled ? "not-allowed" : "pointer",
233
234
  triggerTabIndex: isDisabled ? -1 : 0,
234
- showAddOption: true,
235
+ showAddOption: !isDisabled && !props.noAdd,
235
236
  addOptionLabel: props.addOption?.label || "Add tag",
236
237
  hasDraftChanges,
237
238
  submitDisabled: isDisabled,
@@ -1,4 +1,5 @@
1
1
  import { css } from "../common.js";
2
+ import { calculatePopoverPosition } from "../common/popover.js";
2
3
 
3
4
  const CONTENT_WRAPPER_ATTR = "data-rtgl-popover-content";
4
5
  const DEFAULT_CONTENT_STYLE = "min-width: 200px; max-width: 400px; box-sizing: border-box;";
@@ -115,6 +116,18 @@ class RettangoliPopoverElement extends HTMLElement {
115
116
 
116
117
  // Track if we're open
117
118
  this._isOpen = false;
119
+ this._positionFrameId = null;
120
+ this._revealFrameId = null;
121
+ this._positionVersion = 0;
122
+ this._isObservingResize = false;
123
+ this._observedContentWrapper = null;
124
+ this._resizeObserver = typeof ResizeObserver === "function"
125
+ ? new ResizeObserver(() => {
126
+ if (this._isOpen) {
127
+ this._schedulePositionUpdate();
128
+ }
129
+ })
130
+ : null;
118
131
  }
119
132
 
120
133
  _emitClose() {
@@ -153,6 +166,9 @@ class RettangoliPopoverElement extends HTMLElement {
153
166
  }
154
167
 
155
168
  disconnectedCallback() {
169
+ this._cancelScheduledPositionUpdate();
170
+ this._stopResizeObserver();
171
+
156
172
  // Clean up dialog if it's open
157
173
  if (this._isOpen && this._dialogElement.open) {
158
174
  this._dialogElement.close();
@@ -170,7 +186,7 @@ class RettangoliPopoverElement extends HTMLElement {
170
186
  this._hide();
171
187
  }
172
188
  } else if ((name === 'x' || name === 'y' || name === 'place') && this._isOpen) {
173
- this._updatePosition();
189
+ this._schedulePositionUpdate();
174
190
  } else if (name === 'no-overlay' && oldValue !== newValue && this._isOpen) {
175
191
  this._hide();
176
192
  this._show();
@@ -265,7 +281,7 @@ class RettangoliPopoverElement extends HTMLElement {
265
281
  }
266
282
 
267
283
  if (reposition && this._isOpen) {
268
- this._updatePosition();
284
+ this._schedulePositionUpdate();
269
285
  }
270
286
  }
271
287
 
@@ -281,6 +297,7 @@ class RettangoliPopoverElement extends HTMLElement {
281
297
  }
282
298
 
283
299
  this._isOpen = true;
300
+ this._startResizeObserver();
284
301
 
285
302
  // Show the dialog using setTimeout to ensure it's in the DOM
286
303
  if (!this._dialogElement.open) {
@@ -292,19 +309,20 @@ class RettangoliPopoverElement extends HTMLElement {
292
309
  this._dialogElement.showModal();
293
310
  }
294
311
  }
312
+
313
+ this._schedulePositionUpdate();
295
314
  }, 0);
315
+ } else {
316
+ this._schedulePositionUpdate();
296
317
  }
297
-
298
- // Update position after dialog is shown
299
- requestAnimationFrame(() => {
300
- this._updatePosition();
301
- });
302
318
  }
303
319
  }
304
320
 
305
321
  _hide() {
306
322
  if (this._isOpen) {
307
323
  this._isOpen = false;
324
+ this._cancelScheduledPositionUpdate();
325
+ this._stopResizeObserver();
308
326
 
309
327
  // Close the dialog
310
328
  if (this._dialogElement.open) {
@@ -319,18 +337,83 @@ class RettangoliPopoverElement extends HTMLElement {
319
337
  }
320
338
  }
321
339
 
322
- _updatePosition() {
323
- const x = parseFloat(this.getAttribute('x') || '0');
324
- const y = parseFloat(this.getAttribute('y') || '0');
325
- const place = this.getAttribute('place') || 'bs';
340
+ _startResizeObserver() {
341
+ if (!this._resizeObserver) {
342
+ return;
343
+ }
344
+
345
+ if (!this._isObservingResize) {
346
+ this._resizeObserver.observe(this._popoverContainer);
347
+ this._isObservingResize = true;
348
+ }
349
+
350
+ if (this._contentWrapper && this._observedContentWrapper !== this._contentWrapper) {
351
+ if (this._observedContentWrapper) {
352
+ this._resizeObserver.unobserve(this._observedContentWrapper);
353
+ }
354
+
355
+ this._resizeObserver.observe(this._contentWrapper);
356
+ this._observedContentWrapper = this._contentWrapper;
357
+ }
358
+ }
359
+
360
+ _stopResizeObserver() {
361
+ this._resizeObserver?.disconnect();
362
+ this._isObservingResize = false;
363
+ this._observedContentWrapper = null;
364
+ }
365
+
366
+ _cancelScheduledPositionUpdate() {
367
+ if (this._positionFrameId !== null) {
368
+ cancelAnimationFrame(this._positionFrameId);
369
+ this._positionFrameId = null;
370
+ }
371
+
372
+ if (this._revealFrameId !== null) {
373
+ cancelAnimationFrame(this._revealFrameId);
374
+ this._revealFrameId = null;
375
+ }
376
+
377
+ this._positionVersion += 1;
378
+ this.removeAttribute('positioned');
379
+ }
380
+
381
+ _readCoordinateAttr(name) {
382
+ const value = parseFloat(this.getAttribute(name) || '0');
383
+ return Number.isFinite(value) ? value : 0;
384
+ }
385
+
386
+ _schedulePositionUpdate() {
387
+ if (!this._isOpen) {
388
+ return;
389
+ }
326
390
 
327
391
  // Remove positioned attribute to hide during repositioning
328
392
  this.removeAttribute('positioned');
393
+ this._positionVersion += 1;
394
+
395
+ if (this._positionFrameId !== null) {
396
+ return;
397
+ }
398
+
399
+ this._positionFrameId = requestAnimationFrame(() => {
400
+ this._positionFrameId = null;
401
+
402
+ if (!this._isOpen) {
403
+ return;
404
+ }
405
+
406
+ if (!this._dialogElement.open) {
407
+ this._schedulePositionUpdate();
408
+ return;
409
+ }
329
410
 
330
- // Calculate position based on place
331
- // We'll position after the popover is rendered to get its dimensions
332
- requestAnimationFrame(() => {
333
411
  this._syncContentWrapper({ reposition: false });
412
+ this._startResizeObserver();
413
+
414
+ const x = this._readCoordinateAttr('x');
415
+ const y = this._readCoordinateAttr('y');
416
+ const place = this.getAttribute('place') || 'bs';
334
417
  const rect = this._popoverContainer.getBoundingClientRect();
335
418
  const { left, top } = this._calculatePosition(x, y, rect.width, rect.height, place);
336
419
 
@@ -339,78 +422,27 @@ class RettangoliPopoverElement extends HTMLElement {
339
422
  this._popoverContainer.style.top = `${top}px`;
340
423
 
341
424
  // Then make visible in next frame to prevent flicker
342
- requestAnimationFrame(() => {
343
- this.setAttribute('positioned', '');
425
+ const revealVersion = this._positionVersion;
426
+ this._revealFrameId = requestAnimationFrame(() => {
427
+ this._revealFrameId = null;
428
+
429
+ if (this._isOpen && this._positionVersion === revealVersion) {
430
+ this.setAttribute('positioned', '');
431
+ }
344
432
  });
345
433
  });
346
434
  }
347
435
 
348
436
  _calculatePosition(x, y, width, height, place) {
349
- const offset = 8; // Small offset from the cursor
350
- let left = x;
351
- let top = y;
352
-
353
- switch (place) {
354
- case 't':
355
- left = x - width / 2;
356
- top = y - height - offset;
357
- break;
358
- case 'ts':
359
- left = x;
360
- top = y - height - offset;
361
- break;
362
- case 'te':
363
- left = x - width;
364
- top = y - height - offset;
365
- break;
366
- case 'r':
367
- left = x + offset;
368
- top = y - height / 2;
369
- break;
370
- case 'rs':
371
- left = x + offset;
372
- top = y;
373
- break;
374
- case 're':
375
- left = x + offset;
376
- top = y - height;
377
- break;
378
- case 'b':
379
- left = x - width / 2;
380
- top = y + offset;
381
- break;
382
- case 'bs':
383
- left = x;
384
- top = y + offset;
385
- break;
386
- case 'be':
387
- left = x - width;
388
- top = y + offset;
389
- break;
390
- case 'l':
391
- left = x - width - offset;
392
- top = y - height / 2;
393
- break;
394
- case 'ls':
395
- left = x - width - offset;
396
- top = y;
397
- break;
398
- case 'le':
399
- left = x - width - offset;
400
- top = y - height;
401
- break;
402
- default:
403
- left = x;
404
- top = y + offset;
405
- break;
406
- }
407
-
408
- // Ensure popover stays within viewport
409
- const padding = 8;
410
- left = Math.max(padding, Math.min(left, window.innerWidth - width - padding));
411
- top = Math.max(padding, Math.min(top, window.innerHeight - height - padding));
412
-
413
- return { left, top };
437
+ return calculatePopoverPosition({
438
+ x,
439
+ y,
440
+ width,
441
+ height,
442
+ place,
443
+ viewportWidth: window.innerWidth,
444
+ viewportHeight: window.innerHeight,
445
+ });
414
446
  }
415
447
 
416
448
 
@@ -422,6 +454,10 @@ class RettangoliPopoverElement extends HTMLElement {
422
454
  get content() {
423
455
  return this._contentWrapper;
424
456
  }
457
+
458
+ refreshContent() {
459
+ this._syncContentWrapper();
460
+ }
425
461
  }
426
462
 
427
463
  // Export factory function to maintain API compatibility