phoenix-ui-components 4.1.0 → 4.2.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.
@@ -800,14 +800,20 @@ class NotificationToastComponent {
800
800
  class DownloadAnimationDialogComponent {
801
801
  data;
802
802
  progress = 0;
803
+ progressSub;
803
804
  constructor(data) {
804
805
  this.data = data;
805
806
  }
806
807
  ngOnInit() {
807
- this.data.progress$.subscribe((val) => {
808
+ this.progressSub = this.data.progress$.subscribe((val) => {
808
809
  this.progress = val;
809
810
  });
810
811
  }
812
+ ngOnDestroy() {
813
+ // `progress$` is a BehaviorSubject that is never completed, so the
814
+ // subscription outlives the dialog unless it is dropped here.
815
+ this.progressSub?.unsubscribe();
816
+ }
811
817
  static ɵfac = function DownloadAnimationDialogComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || DownloadAnimationDialogComponent)(i0.ɵɵdirectiveInject(MAT_DIALOG_DATA)); };
812
818
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: DownloadAnimationDialogComponent, selectors: [["app-download-animation-dialog"]], standalone: false, decls: 12, vars: 5, consts: [[1, "loader-wrapper", "d-flex", "flex-column", "justify-content-center", "align-items-center", "w-100", "h-100", "p-5", "text-center"], ["src", "assets/images/logo-small.svg", "alt", "Loader"], [1, "mt-5", "text-light"], [1, "text-muted"], [1, "loading-bar-container"], ["mode", "determinate", 3, "value"], [1, "progress-text", "text-light", "mt-2"]], template: function DownloadAnimationDialogComponent_Template(rf, ctx) { if (rf & 1) {
813
819
  i0.ɵɵelementStart(0, "div", 0);
@@ -1042,13 +1048,21 @@ class AnimateCameraComponent {
1042
1048
  dialogRef.close();
1043
1049
  });
1044
1050
  };
1045
- if (preset) {
1046
- this.animatePreset(preset, onEnd);
1047
- }
1048
- else {
1049
- this.animateCamera(onEnd);
1051
+ // `animateCamera` refuses to start while another animation is running. If
1052
+ // it does not start, nothing will ever call `onEnd`, so the interval would
1053
+ // run forever behind a dialog the user cannot dismiss.
1054
+ const started = preset
1055
+ ? this.animatePreset(preset, onEnd)
1056
+ : this.animateCamera(onEnd);
1057
+ if (!started) {
1058
+ clearInterval(interval);
1059
+ recorder.stopRecording(() => dialogRef.close());
1050
1060
  }
1051
1061
  }
1062
+ /**
1063
+ * Animate through the given preset.
1064
+ * @returns Whether the animation was started.
1065
+ */
1052
1066
  animatePreset(preset, onEnd) {
1053
1067
  this.setDetectorOpacity(0.2);
1054
1068
  this.eventDisplay.animatePreset(this.animationPresets[preset], () => {
@@ -1056,16 +1070,24 @@ class AnimateCameraComponent {
1056
1070
  if (onEnd)
1057
1071
  onEnd();
1058
1072
  });
1073
+ return true;
1059
1074
  }
1075
+ /**
1076
+ * Animate the camera through the event.
1077
+ * @returns Whether the animation was started. It is refused while another
1078
+ * animation is already running.
1079
+ */
1060
1080
  animateCamera(onEnd) {
1061
- if (!this.isAnimating) {
1062
- this.isAnimating = true;
1063
- this.eventDisplay.animateThroughEvent([11976, 7262, 11927], 3000, () => {
1064
- this.isAnimating = false;
1065
- if (onEnd)
1066
- onEnd();
1067
- });
1081
+ if (this.isAnimating) {
1082
+ return false;
1068
1083
  }
1084
+ this.isAnimating = true;
1085
+ this.eventDisplay.animateThroughEvent([11976, 7262, 11927], 3000, () => {
1086
+ this.isAnimating = false;
1087
+ if (onEnd)
1088
+ onEnd();
1089
+ });
1090
+ return true;
1069
1091
  }
1070
1092
  setDetectorOpacity(opacity) {
1071
1093
  const sceneManager = this.eventDisplay.getThreeManager().getSceneManager();
@@ -2553,7 +2575,15 @@ class IOOptionsDialogComponent {
2553
2575
  }
2554
2576
  async handleROOTInput(files) {
2555
2577
  const rootObjectName = prompt('Enter object name in ROOT file');
2556
- await this.eventDisplay.loadRootGeometry(URL.createObjectURL(files[0]), rootObjectName, files[0].name.split('.')[0]);
2578
+ // The object URL pins the whole file in memory, so release it once the
2579
+ // geometry has been read - ROOT files are routinely hundreds of MB.
2580
+ const objectUrl = URL.createObjectURL(files[0]);
2581
+ try {
2582
+ await this.eventDisplay.loadRootGeometry(objectUrl, rootObjectName, files[0].name.split('.')[0]);
2583
+ }
2584
+ finally {
2585
+ URL.revokeObjectURL(objectUrl);
2586
+ }
2557
2587
  this.onClose();
2558
2588
  }
2559
2589
  async handleRootJSONInput(files) {
@@ -2561,7 +2591,13 @@ class IOOptionsDialogComponent {
2561
2591
  return;
2562
2592
  }
2563
2593
  const name = files[0].name.split('.')[0];
2564
- await this.eventDisplay.loadRootJSONGeometry(URL.createObjectURL(files[0]), name);
2594
+ const objectUrl = URL.createObjectURL(files[0]);
2595
+ try {
2596
+ await this.eventDisplay.loadRootJSONGeometry(objectUrl, name);
2597
+ }
2598
+ finally {
2599
+ URL.revokeObjectURL(objectUrl);
2600
+ }
2565
2601
  this.onClose();
2566
2602
  }
2567
2603
  async handlePHYSLITEInput(files) {
@@ -3373,21 +3409,49 @@ class CartesianGridConfigComponent {
3373
3409
  shiftCartesianGridByPointer() {
3374
3410
  this.shiftGrid = true;
3375
3411
  this.eventDisplay.getUIManager().shiftCartesianGridByPointer();
3412
+ // Drop any subscriptions from a previous shift before starting a new one.
3413
+ this.clearShiftSubscriptions();
3376
3414
  this.originChangedSub = this.eventDisplay
3377
3415
  .getThreeManager()
3378
3416
  .originChanged.subscribe((intersect) => {
3379
3417
  this.translateGrid(intersect);
3380
3418
  });
3419
+ // `stopShifting` may emit synchronously during `subscribe`, in which case
3420
+ // the handler runs before `stopShiftingSub` has been assigned. Track that
3421
+ // with a flag so the teardown still happens (and never dereferences null).
3422
+ let stoppedDuringSubscribe = false;
3381
3423
  this.stopShiftingSub = this.eventDisplay
3382
3424
  .getThreeManager()
3383
3425
  .stopShifting.subscribe((stop) => {
3384
- if (stop) {
3385
- this.originChangedSub.unsubscribe();
3386
- this.stopShiftingSub.unsubscribe();
3426
+ if (!stop) {
3427
+ return;
3428
+ }
3429
+ if (this.stopShiftingSub) {
3430
+ this.clearShiftSubscriptions();
3431
+ }
3432
+ else {
3433
+ stoppedDuringSubscribe = true;
3387
3434
  }
3388
3435
  });
3436
+ if (stoppedDuringSubscribe) {
3437
+ this.clearShiftSubscriptions();
3438
+ }
3389
3439
  this.onClose();
3390
3440
  }
3441
+ /**
3442
+ * Unsubscribe from the grid-shifting subscriptions, if any are active.
3443
+ */
3444
+ clearShiftSubscriptions() {
3445
+ this.originChangedSub?.unsubscribe();
3446
+ this.originChangedSub = null;
3447
+ this.stopShiftingSub?.unsubscribe();
3448
+ this.stopShiftingSub = null;
3449
+ }
3450
+ ngOnDestroy() {
3451
+ // The dialog closes as soon as pointer shifting starts, so without this the
3452
+ // subscriptions outlive the component and leak on every reopen.
3453
+ this.clearShiftSubscriptions();
3454
+ }
3391
3455
  shiftCartesianGridByValues(position) {
3392
3456
  this.translateGrid(position);
3393
3457
  this.eventDisplay.getThreeManager().originChangedEmit(position);
@@ -3664,7 +3728,9 @@ class ViewOptionsComponent {
3664
3728
  this.eventDisplay.getUIManager().show3DDistance(change.checked);
3665
3729
  }
3666
3730
  ngOnDestroy() {
3667
- this.sub.unsubscribe();
3731
+ // The component can be destroyed before ngOnInit has created the
3732
+ // subscription, so guard rather than dereferencing it unconditionally.
3733
+ this.sub?.unsubscribe();
3668
3734
  }
3669
3735
  static ɵfac = function ViewOptionsComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ViewOptionsComponent)(i0.ɵɵdirectiveInject(EventDisplayService), i0.ɵɵdirectiveInject(i1$1.MatDialog)); };
3670
3736
  static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ViewOptionsComponent, selectors: [["app-view-options"]], viewQuery: function ViewOptionsComponent_Query(rf, ctx) { if (rf & 1) {