@preference-sl/pref-viewer 2.11.0-beta.8 → 2.11.0-beta.9

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": "@preference-sl/pref-viewer",
3
- "version": "2.11.0-beta.8",
3
+ "version": "2.11.0-beta.9",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -72,7 +72,6 @@ export default class BabylonJSController {
72
72
  // References to parent custom elements
73
73
  #prefViewer3D = undefined;
74
74
  #prefViewer = undefined;
75
- #prefViewerDialog = null;
76
75
 
77
76
  // Babylon.js core objects
78
77
  #engine = null;
@@ -862,6 +861,24 @@ export default class BabylonJSController {
862
861
  return detail;
863
862
  }
864
863
 
864
+ /**
865
+ * Appends the current date and time to the provided name string in the format: name_YYYY-MM-DD_HH.mm.ss
866
+ * @private
867
+ * @param {string} name - The base name to which the date and time will be appended.
868
+ * @returns {string} The name with the appended date and time.
869
+ */
870
+ #addDateToName(name) {
871
+ const now = new Date();
872
+ const year = now.getFullYear();
873
+ const month = (now.getMonth() + 1).toString().padStart(2, "0");
874
+ const day = now.getDate().toString().padStart(2, "0");
875
+ const hours = now.getHours().toString().padStart(2, "0");
876
+ const minutes = now.getMinutes().toString().padStart(2, "0");
877
+ const seconds = now.getSeconds().toString().padStart(2, "0");
878
+ name = `${name}_${year}-${month}-${day}_${hours}.${minutes}.${seconds}`;
879
+ return name;
880
+ }
881
+
865
882
  /**
866
883
  * Generates and downloads a ZIP file with the specified folder name and files.
867
884
  * @private
@@ -874,16 +891,7 @@ export default class BabylonJSController {
874
891
  * Uses JSZip to create the ZIP and triggers a browser download.
875
892
  */
876
893
  #downloadZip(files, name = "files", comment = "", addDateInName = false) {
877
- if (addDateInName) {
878
- const now = new Date();
879
- const year = now.getFullYear();
880
- const month = (now.getMonth() + 1).toString().padStart(2, "0");
881
- const day = now.getDate().toString().padStart(2, "0");
882
- const hours = now.getHours().toString().padStart(2, "0");
883
- const minutes = now.getMinutes().toString().padStart(2, "0");
884
- const seconds = now.getSeconds().toString().padStart(2, "0");
885
- name = `${name}_${year}-${month}-${day}_${hours}.${minutes}.${seconds}`;
886
- }
894
+ name = addDateInName ? this.#addDateToName(name) : name;
887
895
 
888
896
  const JSZip = require("jszip");
889
897
  const zip = new JSZip();
@@ -915,56 +923,39 @@ export default class BabylonJSController {
915
923
  if (!this.#prefViewer) {
916
924
  return;
917
925
  }
918
- this.#prefViewerDialog = document.createElement("pref-viewer-dialog");
919
- this.#prefViewer.shadowRoot.appendChild(this.#prefViewerDialog);
920
-
921
- const content = document.createElement("div");
922
- content.innerHTML = `
923
- <form id="download-dialog-form" style="display:flex;flex-direction:column;gap:15px;">
924
- <h3 style="margin: 0 0 5px; 0">Download 3D Scene</h3>
925
- <h4 style="margin:0;">Content</h4>
926
- <div style="display:flex;flex-direction:row;gap:15px;margin:0 10px 0 10px;">
926
+
927
+ const header = "Download 3D Scene";
928
+ const content = `
929
+ <form slot="content" id="download-dialog-form" style="display:flex;flex-direction:column;gap:16px;">
930
+ <h4>Content</h4>
931
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
927
932
  <label><input type="radio" name="content" value="1"> Model</label>
928
933
  <label><input type="radio" name="content" value="2"> Scene</label>
929
934
  <label><input type="radio" name="content" value="0" checked> Both</label>
930
935
  </div>
931
- <h4 style="margin:0;">Format</h4>
932
- <div style="display:flex;flex-direction:row;gap:15px;margin: 0 10px 0 10px;">
936
+ <h4>Format</h4>
937
+ <div style="display:flex;flex-direction:row;gap:16px;margin:0 8px 0 8px;">
933
938
  <label><input type="radio" name="format" value="gltf" checked> glTF (ZIP)</label>
934
939
  <label><input type="radio" name="format" value="glb"> GLB</label>
935
940
  <label><input type="radio" name="format" value="usdz"> USDZ</label>
936
941
  </div>
937
- <div style="display:flex;gap:15px;justify-content:stretch;margin-top:10px;">
938
- <button type="button" id="download-dialog-download">Download</button>
939
- <button type="button" id="download-dialog-cancel">Cancel</button>
940
- </div>
941
- <style>
942
- #download-dialog-download,
943
- #download-dialog-cancel {
944
- width: 100%;
945
- padding: 8px 20px;
946
- font-size: 1.1rem;
947
- border-radius: 6px;
948
- border: none;
949
- background: #eee;
950
- cursor: pointer;
951
- transition: background 0.2s;
952
- }
953
- #download-dialog-download:hover,
954
- #download-dialog-cancel:hover {
955
- background: #e0e0e0;
956
- }
957
- </style>
958
942
  </form>`;
943
+ const footer = `
944
+ <button type="button" class="primary" id="download-dialog-download">Download</button>
945
+ <button type="button" id="download-dialog-cancel">Cancel</button>`;
959
946
 
960
- this.#prefViewerDialog.open(content);
947
+ const dialog = this.#prefViewer.openDialog(header, content, footer);
948
+
949
+ if (!dialog) {
950
+ return;
951
+ }
961
952
 
962
953
  // Button event handlers
963
- const form = content.querySelector("#download-dialog-form");
964
- const downloadBtn = content.querySelector("#download-dialog-download");
965
- const cancelBtn = content.querySelector("#download-dialog-cancel");
954
+ const form = dialog.querySelector("#download-dialog-form");
955
+ const downloadButton = dialog.querySelector("#download-dialog-download");
956
+ const cancelButton = dialog.querySelector("#download-dialog-cancel");
966
957
 
967
- downloadBtn.onclick = () => {
958
+ downloadButton.onclick = () => {
968
959
  const contentValue = form.content.value;
969
960
  const formatValue = form.format.value;
970
961
  switch (formatValue) {
@@ -978,14 +969,12 @@ export default class BabylonJSController {
978
969
  this.downloadUSDZ(Number(contentValue));
979
970
  break;
980
971
  }
981
- this.#prefViewerDialog.close();
972
+ this.#prefViewer.closeDialog();
982
973
  };
983
974
 
984
- cancelBtn.onclick = () => {
985
- this.#prefViewerDialog.close();
975
+ cancelButton.onclick = () => {
976
+ this.#prefViewer.closeDialog();
986
977
  };
987
-
988
- this.#prefViewerDialog.open(content);
989
978
  }
990
979
 
991
980
  /**
@@ -1117,6 +1106,7 @@ export default class BabylonJSController {
1117
1106
  default:
1118
1107
  break;
1119
1108
  }
1109
+ fileName = this.#addDateToName(fileName);
1120
1110
  GLTF2Export.GLBAsync(container, fileName, { exportWithoutWaitingForScene: true }).then((glb) => {
1121
1111
  if (glb) {
1122
1112
  glb.downloadFiles();
@@ -1186,6 +1176,7 @@ export default class BabylonJSController {
1186
1176
  default:
1187
1177
  break;
1188
1178
  }
1179
+ fileName = this.#addDateToName(fileName);
1189
1180
  USDZExportAsync(container).then((response) => {
1190
1181
  if (response) {
1191
1182
  Tools.Download(new Blob([response], { type: "model/vnd.usdz+zip" }), `${fileName}.usdz`);
@@ -0,0 +1,39 @@
1
+ /* Variables */
2
+ pref-viewer-2d {
3
+ --pref-viewer-2d-bg-color: #ffffff;
4
+ --pref-viewer-2d-svg-padding: 10px;
5
+ }
6
+
7
+ pref-viewer-2d[visible="true"] {
8
+ display: block;
9
+ }
10
+
11
+ pref-viewer-2d[visible="false"] {
12
+ display: none;
13
+ }
14
+
15
+ pref-viewer-2d {
16
+ grid-column: 1;
17
+ grid-row: 1;
18
+ overflow: hidden;
19
+ min-width: 0;
20
+ min-height: 0;
21
+ align-self: stretch;
22
+ justify-self: stretch;
23
+ background: var(--pref-viewer-2d-bg-color);
24
+ }
25
+
26
+ pref-viewer-2d,
27
+ pref-viewer-2d>div,
28
+ pref-viewer-2d>div>svg {
29
+ width: 100%;
30
+ height: 100%;
31
+ display: block;
32
+ position: relative;
33
+ outline: none;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ pref-viewer-2d>div>svg {
38
+ padding: var(--pref-viewer-2d-svg-padding);
39
+ }
@@ -0,0 +1,28 @@
1
+ pref-viewer-3d[visible="true"] {
2
+ display: block;
3
+ }
4
+
5
+ pref-viewer-3d[visible="false"] {
6
+ display: none;
7
+ }
8
+
9
+ pref-viewer-3d {
10
+ grid-column: 1;
11
+ grid-row: 1;
12
+ overflow: hidden;
13
+ min-width: 0;
14
+ min-height: 0;
15
+ align-self: stretch;
16
+ justify-self: stretch;
17
+ }
18
+
19
+ pref-viewer-3d,
20
+ pref-viewer-3d>div,
21
+ pref-viewer-3d>div>canvas {
22
+ width: 100%;
23
+ height: 100%;
24
+ display: block;
25
+ position: relative;
26
+ outline: none;
27
+ box-sizing: border-box;
28
+ }
@@ -0,0 +1,105 @@
1
+ /* Variables */
2
+ pref-viewer-dialog {
3
+ --brand-color: #ff6700;
4
+ --dialog-general-space: 16px;
5
+ --dialog-bg-color: #ffffff;
6
+ --dialog-backdrop-color: rgba(0, 0, 0, 0.25);
7
+ --dialog-border-color: #e7e7e7;
8
+ --dialog-border-radius: 8px;
9
+ --dialog-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
10
+ --button-default-bg-color: #bbbbbb;
11
+ --button-default-bg-color-hover: #a1a1a1;
12
+ --button-primary-bg-color: color-mix(in oklab, var(--brand-color), white 25%);
13
+ --button-primary-bg-color-hover: var(--brand-color);
14
+ --button-border-radius: 4px;
15
+ --button-padding-horizontal: 16px;
16
+ --button-padding-vertical: 8px;
17
+ }
18
+
19
+ pref-viewer-dialog:not {
20
+ display: none;
21
+ }
22
+
23
+ pref-viewer-dialog[open] {
24
+ font-family: 'Roboto', ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
25
+ grid-row: 1;
26
+ grid-column: 1;
27
+ overflow: hidden;
28
+ min-width: 0;
29
+ min-height: 0;
30
+ align-self: stretch;
31
+ justify-self: stretch;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ background-color: var(--dialog-backdrop-color);
36
+ position: relative;
37
+ z-index: 1000;
38
+ }
39
+
40
+ pref-viewer-dialog>.dialog-wrapper {
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: stretch;
44
+ background: var(--dialog-bg-color);
45
+ border: 1px solid var(--dialog-border-color);
46
+ border-radius: var(--dialog-border-radius);
47
+ box-shadow: var(--dialog-box-shadow);
48
+ padding: 0;
49
+ min-width: 320px;
50
+ max-width: 90%;
51
+ max-height: 90%;
52
+ overflow: auto;
53
+ }
54
+
55
+ pref-viewer-dialog .dialog-header {
56
+ padding: var(--dialog-general-space);
57
+ border-bottom: 1px solid var(--dialog-border-color);
58
+ }
59
+
60
+ pref-viewer-dialog .dialog-header h3 {
61
+ margin: 0;
62
+ font-weight: 500;
63
+ font-size: 1.1em;
64
+ }
65
+
66
+ pref-viewer-dialog .dialog-content {
67
+ padding: var(--dialog-general-space);
68
+ }
69
+
70
+ pref-viewer-dialog .dialog-content h4 {
71
+ margin: 0;
72
+ font-weight: 500;
73
+ font-size: 1.05em;
74
+ }
75
+
76
+ pref-viewer-dialog .dialog-footer {
77
+ border-top: 1px solid var(--dialog-border-color);
78
+ padding: var(--dialog-general-space);
79
+ display: flex;
80
+ gap: var(--dialog-general-space);
81
+ justify-content: stretch;
82
+ }
83
+
84
+ pref-viewer-dialog .dialog-footer button {
85
+ width: 100%;
86
+ font-size: 1em;
87
+ padding: var(--button-padding-vertical) var(--button-padding-horizontal);
88
+ border-radius: var(--button-border-radius);
89
+ border: none;
90
+ background: var(--button-default-bg-color);
91
+ cursor: pointer;
92
+ transition: background 0.2s;
93
+ }
94
+
95
+ pref-viewer-dialog .dialog-footer button.primary {
96
+ background: var(--button-primary-bg-color);
97
+ }
98
+
99
+ pref-viewer-dialog .dialog-footer button:hover {
100
+ background: var(--button-default-bg-color-hover);
101
+ }
102
+
103
+ pref-viewer-dialog .dialog-footer button.primary:hover {
104
+ background: var(--button-primary-bg-color-hover);
105
+ }
@@ -0,0 +1,11 @@
1
+ :host .pref-viewer-wrapper {
2
+ display: grid !important;
3
+ position: relative;
4
+ width: 100%;
5
+ height: 100%;
6
+ grid-template-columns: 1fr;
7
+ grid-template-rows: 1fr;
8
+ grid-gap: 0;
9
+ min-width: 0;
10
+ min-height: 0;
11
+ }
package/src/index.js CHANGED
@@ -54,6 +54,8 @@ import { PrefViewerTask } from "./pref-viewer-task.js";
54
54
  * - downloadSceneGLB(): Downloads the environment as a GLB file.
55
55
  * - downloadSceneGLTF(): Downloads the environment as a glTF ZIP file.
56
56
  * - downloadSceneUSDZ(): Downloads the environment as a USDZ file.
57
+ * - openDialog(title, content, footer): Opens a modal dialog with the specified title, content, and footer.
58
+ * - closeDialog(): Closes the currently open dialog, if any, and removes it from the DOM.
57
59
  *
58
60
  * Public Properties:
59
61
  * - isInitialized: Indicates if the viewer is initialized.
@@ -82,6 +84,7 @@ class PrefViewer extends HTMLElement {
82
84
 
83
85
  #taskQueue = [];
84
86
 
87
+ #wrapper = null;
85
88
  #component2D = null;
86
89
  #component3D = null;
87
90
  #dialog = null;
@@ -165,6 +168,14 @@ class PrefViewer extends HTMLElement {
165
168
  * @returns {void|boolean} Returns false if initialization fails; otherwise void.
166
169
  */
167
170
  connectedCallback() {
171
+ this.#wrapper = document.createElement("div");
172
+ this.#wrapper.classList.add("pref-viewer-wrapper");
173
+ this.shadowRoot.append(this.#wrapper);
174
+
175
+ const style = document.createElement("style");
176
+ style.textContent = `@import "../src/css/pref-viewer.css";`;
177
+ this.shadowRoot.append(style);
178
+
168
179
  this.#createComponent3D();
169
180
  this.#createComponent2D();
170
181
 
@@ -186,7 +197,7 @@ class PrefViewer extends HTMLElement {
186
197
  this.#component2D = document.createElement("pref-viewer-2d");
187
198
  this.#component2D.setAttribute("visible", "false");
188
199
  this.#component2D.addEventListener("drawing-zoom-changed", this.#on2DZoomChanged.bind(this));
189
- this.shadowRoot.appendChild(this.#component2D);
200
+ this.#wrapper.appendChild(this.#component2D);
190
201
  }
191
202
 
192
203
  /**
@@ -198,7 +209,7 @@ class PrefViewer extends HTMLElement {
198
209
  #createComponent3D() {
199
210
  this.#component3D = document.createElement("pref-viewer-3d");
200
211
  this.#component3D.setAttribute("visible", "false");
201
- this.shadowRoot.appendChild(this.#component3D);
212
+ this.#wrapper.appendChild(this.#component3D);
202
213
  }
203
214
 
204
215
  /**
@@ -500,11 +511,14 @@ class PrefViewer extends HTMLElement {
500
511
  this.#component3D?.hide();
501
512
  this.#component2D?.show();
502
513
  } else {
503
- this.#component3D?.show();
504
514
  this.#component2D?.hide();
515
+ this.#component3D?.show();
505
516
  }
506
517
  if (this.getAttribute("mode") !== mode) {
507
518
  this.setAttribute("mode", mode);
519
+ if (this.#dialog) {
520
+ this.closeDialog();
521
+ }
508
522
  }
509
523
  }
510
524
 
@@ -734,7 +748,7 @@ class PrefViewer extends HTMLElement {
734
748
 
735
749
  this.#component3D.downloadModelUSDZ();
736
750
  }
737
-
751
+
738
752
  /**
739
753
  * Initiates download of the current complete scene (3D model and environment) in GLB format.
740
754
  * @public
@@ -809,10 +823,42 @@ class PrefViewer extends HTMLElement {
809
823
  if (!this.#component3D) {
810
824
  return;
811
825
  }
812
-
813
826
  this.#component3D.downloadSceneUSDZ();
814
827
  }
815
828
 
829
+ /**
830
+ * Opens a modal dialog with the specified title, content, and footer.
831
+ * @public
832
+ * @param {string} title - The dialog title to display in the header.
833
+ * @param {string} content - The HTML content to display in the dialog body.
834
+ * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
835
+ * @returns {HTMLElement} The created dialog element.
836
+ * @description
837
+ * If a dialog is already open, it is closed before opening the new one.
838
+ * The dialog is appended to the viewer's shadow DOM and returned for further manipulation.
839
+ */
840
+ openDialog(title, content, footer) {
841
+ if (this.#dialog && this.#dialog.hasAttribute("open")) {
842
+ this.#dialog.close();
843
+ }
844
+ this.#dialog = document.createElement("pref-viewer-dialog");
845
+ this.shadowRoot.querySelector(".pref-viewer-wrapper").appendChild(this.#dialog);
846
+ const opened = this.#dialog.open(title, content, footer);
847
+ return opened ? this.#dialog : null;
848
+ }
849
+
850
+ /**
851
+ * Closes the currently open dialog, if any, and removes it from the DOM.
852
+ * @public
853
+ * @returns {void}
854
+ */
855
+ closeDialog() {
856
+ if (this.#dialog) {
857
+ this.#dialog.close();
858
+ this.#dialog = null;
859
+ }
860
+ }
861
+
816
862
  /**
817
863
  * ---------------------------
818
864
  * Public properties
@@ -144,7 +144,7 @@ export class PrefViewer2D extends HTMLElement {
144
144
  this.#wrapper = document.createElement("div");
145
145
  this.append(this.#wrapper);
146
146
  const style = document.createElement("style");
147
- style.textContent = `pref-viewer-2d[visible="true"] { display: block; } pref-viewer-2d[visible="false"] { display: none; } pref-viewer-2d, pref-viewer-2d > div, pref-viewer-2d > div > svg { width: 100%; height: 100%; display: block; position: relative; outline: none; box-sizing: border-box; } pref-viewer-2d > div > svg { padding: 10px; }`;
147
+ style.textContent = `@import "../src/css/pref-viewer-2d.css";`;
148
148
  this.append(style);
149
149
  }
150
150
 
@@ -160,7 +160,7 @@ export class PrefViewer3D extends HTMLElement {
160
160
  this.#canvas = document.createElement("canvas");
161
161
  this.#wrapper.appendChild(this.#canvas);
162
162
  const style = document.createElement("style");
163
- style.textContent = `pref-viewer-3d[visible="true"] { display: block; } pref-viewer-3d[visible="false"] { display: none; } pref-viewer-3d, pref-viewer-3d > div, pref-viewer-3d > div > canvas { width: 100%; height: 100%; display: block; position: relative; outline: none; box-sizing: border-box; }`;
163
+ style.textContent = `@import "../src/css/pref-viewer-3d.css";`;
164
164
  this.append(style);
165
165
  }
166
166
 
@@ -9,19 +9,22 @@
9
9
  *
10
10
  * Usage:
11
11
  * - Use as a custom HTML element: <pref-viewer-dialog></pref-viewer-dialog>
12
- * - Call open(content) to display the dialog with custom HTML content.
12
+ * - Call open(title, content, footer) to display the dialog with a header, content, and footer.
13
13
  * - Call close() to hide and remove the dialog.
14
14
  *
15
15
  * Methods:
16
16
  * - constructor(): Initializes the custom element.
17
17
  * - connectedCallback(): Called when the element is added to the DOM; sets up DOM and styles.
18
18
  * - disconnectedCallback(): Called when the element is removed from the DOM; cleans up resources.
19
- * - open(content): Displays the dialog and appends the provided content.
19
+ * - open(title, content, footer): Displays the dialog and sets its header, content, and footer.
20
20
  * - close(): Hides and removes the dialog, refocuses the canvas if available.
21
21
  * - #createDOMElements(): Creates the dialog structure and applies styles.
22
22
  */
23
23
  export class PrefViewerDialog extends HTMLElement {
24
24
  #wrapper = null;
25
+ #header = null;
26
+ #content = null;
27
+ #footer = null;
25
28
 
26
29
  /**
27
30
  * Initializes the custom dialog element.
@@ -45,7 +48,9 @@ export class PrefViewerDialog extends HTMLElement {
45
48
  * Cleans up resources and event listeners.
46
49
  * @returns {void}
47
50
  */
48
- disconnectedCallback() {}
51
+ disconnectedCallback() {
52
+ this.removeEventListener("click", this.#closeOnBackdropClick.bind(this));
53
+ }
49
54
 
50
55
  /**
51
56
  * Creates the dialog's DOM structure and applies CSS styles.
@@ -55,51 +60,57 @@ export class PrefViewerDialog extends HTMLElement {
55
60
  */
56
61
  #createDOMElements() {
57
62
  this.#wrapper = document.createElement("div");
63
+ this.#wrapper.classList.add("dialog-wrapper");
64
+ this.#wrapper.innerHTML = `
65
+ <div class="dialog-header"><h3 class="dialog-header-title"></h3></div>
66
+ <div class="dialog-content"></div>
67
+ <div class="dialog-footer"></div>`;
58
68
  this.append(this.#wrapper);
69
+
59
70
  const style = document.createElement("style");
60
- style.textContent = `
61
- pref-viewer-dialog {
62
- display: none;
63
- }
64
- pref-viewer-dialog[open] {
65
- font-family: ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
66
- width: 100%;
67
- height: 100%;
68
- display: flex;
69
- align-items: center;
70
- justify-content: center;
71
- background-color: rgba(0, 0, 0, 0.25);
72
- position: fixed;
73
- top: 0;
74
- left: 0;
75
- z-index: 1000;
76
- }
77
- pref-viewer-dialog > div:first-child {
78
- display: inline-block;
79
- background: #fff;
80
- border-radius: 10px;
81
- box-shadow: 0 4px 24px rgba(0,0,0,0.25);
82
- padding: 15px;
83
- max-width: 90%;
84
- max-height: 90%;
85
- overflow: auto;
86
- }`;
71
+ style.textContent = `@import "../src/css/pref-viewer-dialog.css";`;
87
72
  this.append(style);
88
- this.addEventListener("click", (event) => {
89
- if (event.target === this) {
90
- this.close();
91
- }
92
- });
73
+
74
+ this.addEventListener("click", this.#closeOnBackdropClick.bind(this));
75
+
76
+ this.#header = this.#wrapper.querySelector(".dialog-header");
77
+ this.#content = this.#wrapper.querySelector(".dialog-content");
78
+ this.#footer = this.#wrapper.querySelector(".dialog-footer");
79
+ }
80
+
81
+ #closeOnBackdropClick(event) {
82
+ if (event.target === this) {
83
+ this.close();
84
+ }
93
85
  }
94
86
 
95
87
  /**
96
- * Opens the dialog and appends the provided content.
97
- * @param {HTMLElement} content - The HTML content to display inside the dialog.
98
- * @returns {void}
88
+ * Opens the dialog and sets its header, content, and footer.
89
+ * @param {string} title - The dialog title to display in the header.
90
+ * @param {string} content - The HTML content to display in the dialog body.
91
+ * @param {string} footer - The HTML content to display in the dialog footer (e.g., action buttons).
92
+ * @returns {boolean} True if the dialog was opened, false if no content was provided.
99
93
  */
100
- open(content) {
94
+ open(title = "", content = "", footer = "") {
95
+ if (this.#wrapper === null || this.#header === null || this.#content === null || this.#footer === null) {
96
+ return false;
97
+ }
98
+ if (title === "" && content === "" && footer === "") {
99
+ return false;
100
+ }
101
+
102
+ if (title === "") {
103
+ this.#header.style.display = "none";
104
+ }
105
+ if (footer === "") {
106
+ this.#footer.style.display = "none";
107
+ }
108
+
109
+ this.#header.querySelector(".dialog-header-title").innerHTML = title;
110
+ this.#content.innerHTML = content;
111
+ this.#footer.innerHTML = footer;
101
112
  this.setAttribute("open", "");
102
- this.#wrapper.appendChild(content);
113
+ return true;
103
114
  }
104
115
 
105
116
  /**
@@ -110,12 +121,19 @@ export class PrefViewerDialog extends HTMLElement {
110
121
  this.removeAttribute("open");
111
122
  const parent = this.getRootNode().host;
112
123
  if (parent) {
113
- // Refocus canvas for accessibility
114
- const canvas = parent.shadowRoot.querySelector("canvas");
124
+ // Refocus 3D canvas or 2D component for accessibility
125
+ const canvas = parent.shadowRoot.querySelector("pref-viewer-3d[visible='true'] canvas");
115
126
  if (canvas) {
116
127
  canvas.focus();
128
+ } else {
129
+ const component2D = parent.shadowRoot.querySelector("pref-viewer-2d[visible='true']");
130
+ if (component2D) {
131
+ component2D.focus();
132
+ }
117
133
  }
134
+
118
135
  }
136
+ this.#wrapper = this.#header = this.#content = this.#footer = null;
119
137
  this.remove();
120
138
  }
121
139
  }