@jsenv/navi 0.3.3 → 0.3.5

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.
@@ -8350,10 +8350,9 @@ const getInnerWidth = (element) => {
8350
8350
  installImportMetaCss(import.meta);
8351
8351
  import.meta.css = /* css */ `
8352
8352
  .ui_transition_container {
8353
+ position: relative;
8353
8354
  display: inline-flex;
8354
8355
  flex: 1;
8355
- position: relative;
8356
- overflow: hidden;
8357
8356
  }
8358
8357
 
8359
8358
  .ui_transition_outer_wrapper {
@@ -8362,7 +8361,6 @@ import.meta.css = /* css */ `
8362
8361
  }
8363
8362
 
8364
8363
  .ui_transition_measure_wrapper {
8365
- overflow: hidden;
8366
8364
  display: inline-flex;
8367
8365
  flex: 1;
8368
8366
  }
@@ -11804,9 +11802,26 @@ const openCallout = (
11804
11802
  if (Error.isError(newMessage)) {
11805
11803
  const error = newMessage;
11806
11804
  newMessage = error.message;
11807
- newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
11805
+ if (error.stack) {
11806
+ newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
11807
+ }
11808
+ }
11809
+
11810
+ // Check if the message is a full HTML document (starts with DOCTYPE)
11811
+ if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
11812
+ // Create iframe to isolate the HTML document
11813
+ const iframe = document.createElement("iframe");
11814
+ iframe.style.border = "none";
11815
+ iframe.style.width = "100%";
11816
+ iframe.style.backgroundColor = "white";
11817
+ iframe.srcdoc = newMessage;
11818
+
11819
+ // Clear existing content and add iframe
11820
+ calloutMessageElement.innerHTML = "";
11821
+ calloutMessageElement.appendChild(iframe);
11822
+ } else {
11823
+ calloutMessageElement.innerHTML = newMessage;
11808
11824
  }
11809
- calloutMessageElement.innerHTML = newMessage;
11810
11825
  };
11811
11826
  {
11812
11827
  const handleClickOutside = (event) => {
@@ -11918,6 +11933,9 @@ const openCallout = (
11918
11933
  update(message, { level });
11919
11934
 
11920
11935
  {
11936
+ const documentScrollLeftAtOpen = document.documentElement.scrollLeft;
11937
+ const documentScrollTopAtOpen = document.documentElement.scrollTop;
11938
+
11921
11939
  let positioner;
11922
11940
  let strategy;
11923
11941
  const determine = () => {
@@ -11940,7 +11958,10 @@ const openCallout = (
11940
11958
  }
11941
11959
  positioner?.stop();
11942
11960
  if (newStrategy === "centered") {
11943
- positioner = centerCalloutInViewport(calloutElement);
11961
+ positioner = centerCalloutInViewport(calloutElement, {
11962
+ documentScrollLeftAtOpen,
11963
+ documentScrollTopAtOpen,
11964
+ });
11944
11965
  } else {
11945
11966
  positioner = stickCalloutToAnchor(calloutElement, anchorElement);
11946
11967
  }
@@ -12058,6 +12079,10 @@ import.meta.css = /* css */ `
12058
12079
  word-break: break-word;
12059
12080
  overflow-wrap: anywhere;
12060
12081
  }
12082
+ .navi_callout_message iframe {
12083
+ display: block;
12084
+ margin: 0;
12085
+ }
12061
12086
  .navi_callout_close_button_column {
12062
12087
  display: flex;
12063
12088
  height: 22px;
@@ -12142,7 +12167,10 @@ const createCalloutElement = () => {
12142
12167
  return calloutElement;
12143
12168
  };
12144
12169
 
12145
- const centerCalloutInViewport = (calloutElement) => {
12170
+ const centerCalloutInViewport = (
12171
+ calloutElement,
12172
+ { documentScrollLeftAtOpen, documentScrollTopAtOpen },
12173
+ ) => {
12146
12174
  // Set up initial styles for centered positioning
12147
12175
  const calloutBoxElement = calloutElement.querySelector(".navi_callout_box");
12148
12176
  const calloutFrameElement = calloutElement.querySelector(
@@ -12195,10 +12223,10 @@ const centerCalloutInViewport = (calloutElement) => {
12195
12223
  finalHeight,
12196
12224
  );
12197
12225
 
12198
- // Center in viewport
12226
+ // Center in viewport (accounting for document scroll)
12199
12227
  const viewportWidth = window.innerWidth;
12200
- const left = (viewportWidth - finalWidth) / 2;
12201
- const top = (viewportHeight - finalHeight) / 2;
12228
+ const left = documentScrollLeftAtOpen + (viewportWidth - finalWidth) / 2;
12229
+ const top = documentScrollTopAtOpen + (viewportHeight - finalHeight) / 2;
12202
12230
 
12203
12231
  calloutStyleController.set(calloutElement, {
12204
12232
  opacity: 1,
@@ -12211,7 +12239,6 @@ const centerCalloutInViewport = (calloutElement) => {
12211
12239
 
12212
12240
  // Initial positioning
12213
12241
  updateCenteredPosition();
12214
-
12215
12242
  window.addEventListener("resize", updateCenteredPosition);
12216
12243
 
12217
12244
  // Return positioning function for dynamic updates
@@ -12424,6 +12451,20 @@ const escapeHtml = (string) => {
12424
12451
  .replace(/'/g, "&#039;");
12425
12452
  };
12426
12453
 
12454
+ /**
12455
+ * Checks if a string is a full HTML document (starts with DOCTYPE)
12456
+ * @param {string} content - The content to check
12457
+ * @returns {boolean} - True if it looks like a complete HTML document
12458
+ */
12459
+ const isHtmlDocument = (content) => {
12460
+ if (typeof content !== "string") {
12461
+ return false;
12462
+ }
12463
+ // Trim whitespace and check if it starts with DOCTYPE (case insensitive)
12464
+ const trimmed = content.trim();
12465
+ return /^<!doctype\s+html/i.test(trimmed);
12466
+ };
12467
+
12427
12468
  // It's ok to do this because the element is absolutely positioned
12428
12469
  const cloneCalloutToMeasureNaturalSize = (calloutElement) => {
12429
12470
  // Create invisible clone to measure natural size
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/navi",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Library of components including navigation to create frontend applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -115,9 +115,26 @@ export const openCallout = (
115
115
  if (Error.isError(newMessage)) {
116
116
  const error = newMessage;
117
117
  newMessage = error.message;
118
- newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
118
+ if (error.stack) {
119
+ newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
120
+ }
121
+ }
122
+
123
+ // Check if the message is a full HTML document (starts with DOCTYPE)
124
+ if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
125
+ // Create iframe to isolate the HTML document
126
+ const iframe = document.createElement("iframe");
127
+ iframe.style.border = "none";
128
+ iframe.style.width = "100%";
129
+ iframe.style.backgroundColor = "white";
130
+ iframe.srcdoc = newMessage;
131
+
132
+ // Clear existing content and add iframe
133
+ calloutMessageElement.innerHTML = "";
134
+ calloutMessageElement.appendChild(iframe);
135
+ } else {
136
+ calloutMessageElement.innerHTML = newMessage;
119
137
  }
120
- calloutMessageElement.innerHTML = newMessage;
121
138
  };
122
139
  close_on_click_outside: {
123
140
  const handleClickOutside = (event) => {
@@ -229,6 +246,9 @@ export const openCallout = (
229
246
  update(message, { level });
230
247
 
231
248
  positioning: {
249
+ const documentScrollLeftAtOpen = document.documentElement.scrollLeft;
250
+ const documentScrollTopAtOpen = document.documentElement.scrollTop;
251
+
232
252
  let positioner;
233
253
  let strategy;
234
254
  const determine = () => {
@@ -251,7 +271,10 @@ export const openCallout = (
251
271
  }
252
272
  positioner?.stop();
253
273
  if (newStrategy === "centered") {
254
- positioner = centerCalloutInViewport(calloutElement);
274
+ positioner = centerCalloutInViewport(calloutElement, {
275
+ documentScrollLeftAtOpen,
276
+ documentScrollTopAtOpen,
277
+ });
255
278
  } else {
256
279
  positioner = stickCalloutToAnchor(calloutElement, anchorElement);
257
280
  }
@@ -369,6 +392,10 @@ import.meta.css = /* css */ `
369
392
  word-break: break-word;
370
393
  overflow-wrap: anywhere;
371
394
  }
395
+ .navi_callout_message iframe {
396
+ display: block;
397
+ margin: 0;
398
+ }
372
399
  .navi_callout_close_button_column {
373
400
  display: flex;
374
401
  height: 22px;
@@ -453,7 +480,10 @@ const createCalloutElement = () => {
453
480
  return calloutElement;
454
481
  };
455
482
 
456
- const centerCalloutInViewport = (calloutElement) => {
483
+ const centerCalloutInViewport = (
484
+ calloutElement,
485
+ { documentScrollLeftAtOpen, documentScrollTopAtOpen },
486
+ ) => {
457
487
  // Set up initial styles for centered positioning
458
488
  const calloutBoxElement = calloutElement.querySelector(".navi_callout_box");
459
489
  const calloutFrameElement = calloutElement.querySelector(
@@ -506,10 +536,10 @@ const centerCalloutInViewport = (calloutElement) => {
506
536
  finalHeight,
507
537
  );
508
538
 
509
- // Center in viewport
539
+ // Center in viewport (accounting for document scroll)
510
540
  const viewportWidth = window.innerWidth;
511
- const left = (viewportWidth - finalWidth) / 2;
512
- const top = (viewportHeight - finalHeight) / 2;
541
+ const left = documentScrollLeftAtOpen + (viewportWidth - finalWidth) / 2;
542
+ const top = documentScrollTopAtOpen + (viewportHeight - finalHeight) / 2;
513
543
 
514
544
  calloutStyleController.set(calloutElement, {
515
545
  opacity: 1,
@@ -522,7 +552,6 @@ const centerCalloutInViewport = (calloutElement) => {
522
552
 
523
553
  // Initial positioning
524
554
  updateCenteredPosition();
525
-
526
555
  window.addEventListener("resize", updateCenteredPosition);
527
556
 
528
557
  // Return positioning function for dynamic updates
@@ -735,6 +764,20 @@ const escapeHtml = (string) => {
735
764
  .replace(/'/g, "&#039;");
736
765
  };
737
766
 
767
+ /**
768
+ * Checks if a string is a full HTML document (starts with DOCTYPE)
769
+ * @param {string} content - The content to check
770
+ * @returns {boolean} - True if it looks like a complete HTML document
771
+ */
772
+ const isHtmlDocument = (content) => {
773
+ if (typeof content !== "string") {
774
+ return false;
775
+ }
776
+ // Trim whitespace and check if it starts with DOCTYPE (case insensitive)
777
+ const trimmed = content.trim();
778
+ return /^<!doctype\s+html/i.test(trimmed);
779
+ };
780
+
738
781
  // It's ok to do this because the element is absolutely positioned
739
782
  const cloneCalloutToMeasureNaturalSize = (calloutElement) => {
740
783
  // Create invisible clone to measure natural size
@@ -0,0 +1,182 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Test HTML Document in Callout</title>
6
+ <style>
7
+ body {
8
+ margin: 0;
9
+ padding: 20px;
10
+ font-family: Arial, sans-serif;
11
+ }
12
+ .test-button {
13
+ display: inline-block;
14
+ margin: 10px;
15
+ padding: 10px 20px;
16
+ color: white;
17
+ font-size: 14px;
18
+ background: #007acc;
19
+ border: none;
20
+ border-radius: 4px;
21
+ cursor: pointer;
22
+ }
23
+ .test-section {
24
+ margin: 40px 0;
25
+ padding: 20px;
26
+ border: 1px solid #ddd;
27
+ border-radius: 4px;
28
+ }
29
+ pre {
30
+ padding: 10px;
31
+ font-size: 12px;
32
+ background: #f5f5f5;
33
+ border-radius: 4px;
34
+ overflow-x: auto;
35
+ }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <h1>HTML Document in Callout Test</h1>
40
+
41
+ <div class="test-section">
42
+ <h2>Test 1: Regular HTML content (no DOCTYPE)</h2>
43
+ <button class="test-button" onclick="showRegularHtml()">
44
+ Show Regular HTML
45
+ </button>
46
+ <p>This should display as normal innerHTML (no iframe).</p>
47
+ </div>
48
+
49
+ <div class="test-section">
50
+ <h2>Test 2: Complete HTML document (with DOCTYPE)</h2>
51
+ <button class="test-button" onclick="showHtmlDocument()">
52
+ Show HTML Document
53
+ </button>
54
+ <p>This should display in an iframe to prevent style conflicts.</p>
55
+ </div>
56
+
57
+ <div class="test-section">
58
+ <h2>Test 3: HTML document with conflicting styles</h2>
59
+ <button class="test-button" onclick="showConflictingStylesDocument()">
60
+ Show Document with Conflicting Styles
61
+ </button>
62
+ <p>This demonstrates how iframe isolation prevents style conflicts.</p>
63
+ </div>
64
+
65
+ <script type="module">
66
+ import { openCallout } from "@jsenv/navi";
67
+
68
+ let currentCallout = null;
69
+
70
+ function closeCurrentCallout() {
71
+ if (currentCallout) {
72
+ currentCallout.close();
73
+ currentCallout = null;
74
+ }
75
+ }
76
+
77
+ window.showRegularHtml = () => {
78
+ closeCurrentCallout();
79
+ const regularHtml = `
80
+ <h3>Regular HTML Content</h3>
81
+ <p>This is just regular HTML content without DOCTYPE.</p>
82
+ <ul>
83
+ <li>Item 1</li>
84
+ <li>Item 2</li>
85
+ <li>Item 3</li>
86
+ </ul>
87
+ <p><strong>Note:</strong> This should display as innerHTML, not in an iframe.</p>
88
+ `;
89
+
90
+ currentCallout = openCallout(regularHtml, {
91
+ level: "info",
92
+ debug: true,
93
+ });
94
+ };
95
+
96
+ window.showHtmlDocument = () => {
97
+ closeCurrentCallout();
98
+ const htmlDocument = `<!DOCTYPE html>
99
+ <html>
100
+ <head>
101
+ <meta charset="utf-8">
102
+ <title>Embedded Document</title>
103
+ <style>
104
+ body {
105
+ font-family: Georgia, serif;
106
+ background: linear-gradient(45deg, #e3f2fd, #fff3e0);
107
+ margin: 0;
108
+ padding: 20px;
109
+ }
110
+ h1 { color: #1976d2; }
111
+ .highlight { background: yellow; padding: 2px 4px; }
112
+ </style>
113
+ </head>
114
+ <body>
115
+ <h1>Complete HTML Document</h1>
116
+ <p>This is a <span class="highlight">complete HTML document</span> with its own styles.</p>
117
+ <p>It's displayed in an iframe to prevent any style conflicts with the parent page.</p>
118
+ <div style="border: 2px solid #4caf50; padding: 10px; margin: 10px 0; border-radius: 8px;">
119
+ <strong>Isolated Environment:</strong> This document has its own CSS and doesn't interfere with the callout styles.
120
+ </div>
121
+ </body>
122
+ </html>`;
123
+
124
+ currentCallout = openCallout(htmlDocument, {
125
+ level: "warning",
126
+ debug: true,
127
+ });
128
+ };
129
+
130
+ window.showConflictingStylesDocument = () => {
131
+ closeCurrentCallout();
132
+ const conflictingDocument = `<!DOCTYPE html>
133
+ <html>
134
+ <head>
135
+ <meta charset="utf-8">
136
+ <title>Conflicting Styles</title>
137
+ <style>
138
+ /* These styles would conflict with the main page if not isolated */
139
+ body {
140
+ background: #ff5722;
141
+ color: white;
142
+ font-family: 'Comic Sans MS', cursive;
143
+ margin: 0;
144
+ padding: 15px;
145
+ }
146
+ button {
147
+ background: #4caf50;
148
+ color: white;
149
+ border: none;
150
+ padding: 8px 16px;
151
+ border-radius: 20px;
152
+ cursor: pointer;
153
+ }
154
+ .navi_callout {
155
+ /* This would break the callout if not isolated */
156
+ display: none !important;
157
+ }
158
+ </style>
159
+ </head>
160
+ <body>
161
+ <h1>🎨 Document with Conflicting Styles</h1>
162
+ <p>This document has styles that would completely break the callout if not isolated in an iframe!</p>
163
+ <button onclick="alert('This button works inside the iframe!')">Click me!</button>
164
+ <p><em>Notice how the styles don't affect the callout container.</em></p>
165
+ </body>
166
+ </html>`;
167
+
168
+ currentCallout = openCallout(conflictingDocument, {
169
+ level: "error",
170
+ debug: true,
171
+ });
172
+ };
173
+
174
+ // Close callout on escape key
175
+ document.addEventListener("keydown", (e) => {
176
+ if (e.key === "Escape") {
177
+ closeCurrentCallout();
178
+ }
179
+ });
180
+ </script>
181
+ </body>
182
+ </html>