@jsenv/navi 0.3.3 → 0.3.4

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.
@@ -11806,7 +11806,24 @@ const openCallout = (
11806
11806
  newMessage = error.message;
11807
11807
  newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
11808
11808
  }
11809
- calloutMessageElement.innerHTML = newMessage;
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.minHeight = "200px";
11817
+ iframe.style.maxHeight = "400px";
11818
+ iframe.style.backgroundColor = "white";
11819
+ iframe.srcdoc = newMessage;
11820
+
11821
+ // Clear existing content and add iframe
11822
+ calloutMessageElement.innerHTML = "";
11823
+ calloutMessageElement.appendChild(iframe);
11824
+ } else {
11825
+ calloutMessageElement.innerHTML = newMessage;
11826
+ }
11810
11827
  };
11811
11828
  {
11812
11829
  const handleClickOutside = (event) => {
@@ -11918,6 +11935,9 @@ const openCallout = (
11918
11935
  update(message, { level });
11919
11936
 
11920
11937
  {
11938
+ const documentScrollLeftAtOpen = document.documentElement.scrollLeft;
11939
+ const documentScrollTopAtOpen = document.documentElement.scrollTop;
11940
+
11921
11941
  let positioner;
11922
11942
  let strategy;
11923
11943
  const determine = () => {
@@ -11940,7 +11960,10 @@ const openCallout = (
11940
11960
  }
11941
11961
  positioner?.stop();
11942
11962
  if (newStrategy === "centered") {
11943
- positioner = centerCalloutInViewport(calloutElement);
11963
+ positioner = centerCalloutInViewport(calloutElement, {
11964
+ documentScrollLeftAtOpen,
11965
+ documentScrollTopAtOpen,
11966
+ });
11944
11967
  } else {
11945
11968
  positioner = stickCalloutToAnchor(calloutElement, anchorElement);
11946
11969
  }
@@ -12058,6 +12081,12 @@ import.meta.css = /* css */ `
12058
12081
  word-break: break-word;
12059
12082
  overflow-wrap: anywhere;
12060
12083
  }
12084
+ .navi_callout_message iframe {
12085
+ display: block;
12086
+ margin: 0;
12087
+ border: 1px solid #ddd;
12088
+ border-radius: 4px;
12089
+ }
12061
12090
  .navi_callout_close_button_column {
12062
12091
  display: flex;
12063
12092
  height: 22px;
@@ -12142,7 +12171,10 @@ const createCalloutElement = () => {
12142
12171
  return calloutElement;
12143
12172
  };
12144
12173
 
12145
- const centerCalloutInViewport = (calloutElement) => {
12174
+ const centerCalloutInViewport = (
12175
+ calloutElement,
12176
+ { documentScrollLeftAtOpen, documentScrollTopAtOpen },
12177
+ ) => {
12146
12178
  // Set up initial styles for centered positioning
12147
12179
  const calloutBoxElement = calloutElement.querySelector(".navi_callout_box");
12148
12180
  const calloutFrameElement = calloutElement.querySelector(
@@ -12195,10 +12227,10 @@ const centerCalloutInViewport = (calloutElement) => {
12195
12227
  finalHeight,
12196
12228
  );
12197
12229
 
12198
- // Center in viewport
12230
+ // Center in viewport (accounting for document scroll)
12199
12231
  const viewportWidth = window.innerWidth;
12200
- const left = (viewportWidth - finalWidth) / 2;
12201
- const top = (viewportHeight - finalHeight) / 2;
12232
+ const left = documentScrollLeftAtOpen + (viewportWidth - finalWidth) / 2;
12233
+ const top = documentScrollTopAtOpen + (viewportHeight - finalHeight) / 2;
12202
12234
 
12203
12235
  calloutStyleController.set(calloutElement, {
12204
12236
  opacity: 1,
@@ -12211,7 +12243,6 @@ const centerCalloutInViewport = (calloutElement) => {
12211
12243
 
12212
12244
  // Initial positioning
12213
12245
  updateCenteredPosition();
12214
-
12215
12246
  window.addEventListener("resize", updateCenteredPosition);
12216
12247
 
12217
12248
  // Return positioning function for dynamic updates
@@ -12424,6 +12455,20 @@ const escapeHtml = (string) => {
12424
12455
  .replace(/'/g, "&#039;");
12425
12456
  };
12426
12457
 
12458
+ /**
12459
+ * Checks if a string is a full HTML document (starts with DOCTYPE)
12460
+ * @param {string} content - The content to check
12461
+ * @returns {boolean} - True if it looks like a complete HTML document
12462
+ */
12463
+ const isHtmlDocument = (content) => {
12464
+ if (typeof content !== "string") {
12465
+ return false;
12466
+ }
12467
+ // Trim whitespace and check if it starts with DOCTYPE (case insensitive)
12468
+ const trimmed = content.trim();
12469
+ return /^<!doctype\s+html/i.test(trimmed);
12470
+ };
12471
+
12427
12472
  // It's ok to do this because the element is absolutely positioned
12428
12473
  const cloneCalloutToMeasureNaturalSize = (calloutElement) => {
12429
12474
  // 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.4",
4
4
  "description": "Library of components including navigation to create frontend applications",
5
5
  "repository": {
6
6
  "type": "git",
@@ -117,7 +117,24 @@ export const openCallout = (
117
117
  newMessage = error.message;
118
118
  newMessage += `<pre class="navi_callout_error_stack">${escapeHtml(error.stack)}</pre>`;
119
119
  }
120
- calloutMessageElement.innerHTML = newMessage;
120
+
121
+ // Check if the message is a full HTML document (starts with DOCTYPE)
122
+ if (typeof newMessage === "string" && isHtmlDocument(newMessage)) {
123
+ // Create iframe to isolate the HTML document
124
+ const iframe = document.createElement("iframe");
125
+ iframe.style.border = "none";
126
+ iframe.style.width = "100%";
127
+ iframe.style.minHeight = "200px";
128
+ iframe.style.maxHeight = "400px";
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;
137
+ }
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,12 @@ 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
+ border: 1px solid #ddd;
399
+ border-radius: 4px;
400
+ }
372
401
  .navi_callout_close_button_column {
373
402
  display: flex;
374
403
  height: 22px;
@@ -453,7 +482,10 @@ const createCalloutElement = () => {
453
482
  return calloutElement;
454
483
  };
455
484
 
456
- const centerCalloutInViewport = (calloutElement) => {
485
+ const centerCalloutInViewport = (
486
+ calloutElement,
487
+ { documentScrollLeftAtOpen, documentScrollTopAtOpen },
488
+ ) => {
457
489
  // Set up initial styles for centered positioning
458
490
  const calloutBoxElement = calloutElement.querySelector(".navi_callout_box");
459
491
  const calloutFrameElement = calloutElement.querySelector(
@@ -506,10 +538,10 @@ const centerCalloutInViewport = (calloutElement) => {
506
538
  finalHeight,
507
539
  );
508
540
 
509
- // Center in viewport
541
+ // Center in viewport (accounting for document scroll)
510
542
  const viewportWidth = window.innerWidth;
511
- const left = (viewportWidth - finalWidth) / 2;
512
- const top = (viewportHeight - finalHeight) / 2;
543
+ const left = documentScrollLeftAtOpen + (viewportWidth - finalWidth) / 2;
544
+ const top = documentScrollTopAtOpen + (viewportHeight - finalHeight) / 2;
513
545
 
514
546
  calloutStyleController.set(calloutElement, {
515
547
  opacity: 1,
@@ -522,7 +554,6 @@ const centerCalloutInViewport = (calloutElement) => {
522
554
 
523
555
  // Initial positioning
524
556
  updateCenteredPosition();
525
-
526
557
  window.addEventListener("resize", updateCenteredPosition);
527
558
 
528
559
  // Return positioning function for dynamic updates
@@ -735,6 +766,20 @@ const escapeHtml = (string) => {
735
766
  .replace(/'/g, "&#039;");
736
767
  };
737
768
 
769
+ /**
770
+ * Checks if a string is a full HTML document (starts with DOCTYPE)
771
+ * @param {string} content - The content to check
772
+ * @returns {boolean} - True if it looks like a complete HTML document
773
+ */
774
+ const isHtmlDocument = (content) => {
775
+ if (typeof content !== "string") {
776
+ return false;
777
+ }
778
+ // Trim whitespace and check if it starts with DOCTYPE (case insensitive)
779
+ const trimmed = content.trim();
780
+ return /^<!doctype\s+html/i.test(trimmed);
781
+ };
782
+
738
783
  // It's ok to do this because the element is absolutely positioned
739
784
  const cloneCalloutToMeasureNaturalSize = (calloutElement) => {
740
785
  // 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>