@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.
package/dist/jsenv_navi.js
CHANGED
|
@@ -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
|
-
|
|
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 = (
|
|
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, "'");
|
|
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
|
@@ -115,9 +115,26 @@ export const openCallout = (
|
|
|
115
115
|
if (Error.isError(newMessage)) {
|
|
116
116
|
const error = newMessage;
|
|
117
117
|
newMessage = error.message;
|
|
118
|
-
|
|
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 = (
|
|
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, "'");
|
|
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>
|